Git 学习笔记(CheatSheet)(一)

[TOC]

Git 思维导图

简介

当我们进行项目开发时,都会使用『版本控制系统(Version Control Systems,VCS)』托管项目源码,版本控制系统记录了项目源码的历史迭代过程,可以很方便让我们比对不同版本之间源码差异,以及在任意时刻回滚到某一个历史版本中...对项目的持续迭代过程非常有帮助。

传统的版本控制多使用『集中化版本控制系统(Centralized Version Control Systems,CVCS)』,这些系统将源码存放于一个单一的中央服务器中,方便进行集中化管理。协同工作的开发人员通过网络连接到这台服务器,下载最新源码或提交更新。

CVCS 优点是协同流程简单,项目管理方便。但缺点是当存在网络故障或服务器宕机时,无法开展协同工作且数据存在丢失风险。
因此,『分布式版本控制系统(Distributed Version Control System,DVCS)』便应运而生了。

传统的集中化版本控制系统都要求存在一个公共的中央服务器,用以存储项目源码历史记录,而分布式系统不存中央结点概念,或者说每个结点都是中央结点,都存储了同一份源码的完整历史记录,因此任何一个结点数据丢失,不会影响到其他结点数据。

当前最流行的分布式版本控制器应当就是 Git 了,它的最初版本是由 Linux 之父 Linus Torvalds 进行开发的。
:DVCS 解决了 CVCS 数据不安全问题,但由于不区分主从结点,每个结点都是高度自治的,因此在协同开发上不是很方便。这其实也是分布式系统存在的共同问题,而要解决这个问题,其实也不难,只需提供一个端对端发现机制即可,这样分布式网络中的任意一个结点就可通过这个发现机制搜索到目标仓库结点,从而可以开展协同工作。对于 Git 而言,当前使用最多的第三方端对端发现机制就是 Github,Github 提供了一个仓库托管、发现和管理平台,其他结点可从该平台上搜索找到感兴趣的目标仓库,进行协同开发。

本篇博文主要介绍使用 Git 进行版本控制相关内容,对相关场景下的 Git 使用进行简介,尽量涉及常用的 Git 命令,可将本篇博文作为 Git 使用的一个速查表(CheatSheet)。

在阅读本篇博文前,强烈建议先阅读下本人写的另一篇介绍 Git 底层实现原理的博文:Git 内部实现原理剖析,了解下 Git 的底层实现原理可以让我们对所使用的命令有更充分的认识,避免死记命令,才能更加游刃有余。

简单来讲,Git 的本质是一个『内容寻址文件系统(Content-Addressable Filesystem)』,它只关注被追踪文件的内容。当暂存一个文件时,Git 会首先对该文件内容以一定的格式进行 SHA-1 计算,得到文件内容的一个数字摘要,然后将文件内容存储到本地对象数据库中。数字摘要从理论上说,几乎唯一的标识了一份内容,因此,依据该数据摘要,我们就可以获取得到其对应的文件内容,所以 Git 的核心部分其实就是一个简单的键值对数据库(Key-Value Data Store)。
这其实也是 Git 与其他版本控制系统最主要的差别,其他的版本控制系统多采用基于差异(delta-based)的机制实现版本控制,而 Git 是以 全量快照 方式保存文件内容。换言之,集中化版本控制系统是基于增量式文件系统,而 Git 采用的是自己实现的一套全量式存储文件系统。

安装

  • 源码安装:为了能安装到最新的版本,本文采用源码安装 Git,其步骤如下:

    1. 首先下载 git 源码

      # 下载 git 源码
      $ git clone 'https://github.com/git/git.git'
      # 卸载本地 git
      $ sudo apt remove --purge git
      
    2. 安装相关依赖:

      $ sudo apt-get install dh-autoreconf libcurl4-gnutls-dev libexpat1-dev \
        gettext libz-dev libssl-dev
      

      如果要安装 Git 文档的多种格式(doc、html、info),还需添加以下依赖:

      $ sudo apt-get install asciidoc xmlto docbook2x
      

      如果使用的系统是基于 Debian 的发行版(Debian/Ubuntu/Ubuntu-derivatives),还需安装以下依赖:

      $ sudo apt-get install install-info
      
    3. 进入 Git 源码目录,并进行安装:

      $ cd git
      $ make configure
      # 安装到 /usr/local/bin
      $ ./configure --prefix=/usr/local
      # doc info 为文档
      $ make all doc info
      $ sudo make install install-doc install-html install-info
      

    以上,源码安装 Git 已完成。

帮助文档

Git 命令的所有文档可查看:在线文档 - Reference

Git 的官方在线文档在中国可能无法直接访问,此时其实可以直接在终端查看帮助文档,如下所示:

$ man git <verb>    # 法一
$ git help <verb>   # 法二
$ git <verb> --help # 法三
$ git <verb> -h     # 法四

上述前三种方法效果一样,任选其一即可,比如,如果我们想要查看git config的用法,就可以使用如下命令:

$ git help config

该命令会列出git config的详细用法。

以上方法查询得到的命令文档非常详细,而如果想要查看更简洁紧凑的命令用法,可以直接使用-h参数,比如:

$ git config -h

相对来说,-h会更加直观且易读,推荐使用该种查询。

配置

Git 中配置主要分为全局配置和项目本地配置,可通过命令git config设置和查看配置选项,其格式如下:

git config <options>

配置作用域

Git 中存在三种配置作用域:

  • 系统配置:使用命令git config --system可以配置系统变量,配置内容存储在/etc/gitconfig文件中,系统配置对所有用户都生效。
    :可通过如下命令直接打开 Git 系统配置文件:
$ git config --system --edit
  • 全局配置:使用命令git config --global可以设置全局配置变量,配置内容存储在~/.gitconfig~/.config/git/config文件中,全局配置只对当前用户生效,但是对该用户的所有本地仓库都生效。
    :可通过如下命令直接打开 Git 全局配置文件:
$ git config --global --edit
  • 本地仓库配置:使用命令git configgit config --local可以设置仓库本地变量,配置内容存储在本地仓库.git/config文件中,本地仓库配置只对当前仓库生效。
    :可通过如下命令直接打开 Git 本地配置文件:
$ git config --local --edit

:系统配置、全局配置 和 本地仓库配置会依序进行加载,因此后面的配置会覆盖前面相同的配置。

:可通过以下命令查看各个配置选项内容及其所在的配置文件:

$ git config --list --show-origin
file:/home/whyn/.gitconfig      user.name=Why8n
file:.git/config        core.repositoryformatversion=0
...

基础配置

使用 Git 的第一步就是进行一些基础配置,通常会先全局配置用户名和邮箱:

  • 全局配置用户名

    $ git config --global user.name Why8n
    
  • 全局配置邮箱

    $ git config --global user.email whyncai@gmail.com
    
  • 中文乱码解决:Git 中有时候中文可能显示为乱码,此时进行如下配置,可解决大部分中文乱码问题:

    # 解决 git status 中文乱码
    $ git config --global core.quotepath false
    
    # 设置 git gui 界面编码
    $ git config --global gui.encoding utf-8
    
    # 设置 git log 提交内容编码
    $ git config --global i18n.commitencoding utf-8 
    

配置查询

Git 中主要的查询有如下几种:

  • 本地配置查询:使用--local选项:

    $ git config --local --list
    
  • 全局配置查询:使用--global选项:

    $ git config --global --list
    
  • 项目所有配置查询:包含全局配置和本地配置选项:

    $ git config --list
    
  • 单选项查询:比如查询用户邮箱:

    $ git config user.email
    Why8n@gmail.com
    

删除配置

Git 提供了多种方法对配置选项进行删除,这里只介绍一种:

  • 单选项配置删除:比如删除user.email选项:
    $ git config --global --unset user.email
    
    # 同一选项存在多条配置
    $ git config --global --unset-all user.email
    

服务器配置 SSH 公钥

进行版本控制时,通常将源码存储到一个中央服务器中,方便开展协同工作。这里我们使用 Github 平台作为远程中央服务器,存储我们的项目代码。
Github 支持 SSH 协议连接,为了方便使用,可以本地生成一个 SSH 公钥添加到对应的 Github 账户中,这样后续使用时就无需手动输入用户名和密码进行验证,具体步骤如下所示:

  1. 本地生成 SSH 公钥:

    $ ssh-keygen -t ed25519 -C "whyncai@gmail.com"
    Generating public/private ed25519 key pair.
    Enter file in which to save the key (/home/whyn/.ssh/id_ed25519): # 存储路径
    Enter passphrase (empty for no passphrase): # 密钥口令,留空即可。
    Enter same passphrase again:
    Your identification has been saved in /home/whyn/.ssh/id_ed25519.
    Your public key has been saved in /home/whyn/.ssh/id_ed25519.pub.
    

    :生成过程中会要求输入两次密钥口令,我们将其留空即可,这样后续使用密钥时,无需键入口令。

    此时,~/.ssh目录会生成两个新文件,其中,.pub文件为 SSH 公钥,另一个为 SSH 私钥文件。

  2. 拷贝公钥内容:

    $ xclip -selection clipboard < ~/.ssh/id_ed25519.pub
    
  3. 将公钥内容添加到 Github 账号上,步骤如下:
    1). 打开账号设置界面:


    Settings

    2). 左侧用户设置栏选择:SSH and GPS kyes

    SSH and GPG keys

    3). 点击:New SSH key,弹出配置界面:Title域填入自定义标题,Key粘贴公钥内容,最后点击 Add SSH key 即可。

  4. 以上,就已成功添加本机 SSH 公钥到 Github 账户上:


    SSH public key
  5. 此时可以测试使用 SSH 连接到 Github,看下配置是否成功:

    $ ssh -T git@github.com
    # 测试连接成功
    Hi Why8n! You've successfully authenticated, but GitHub does not provide shell access.
    
  6. 最后还有一步,将远程仓库路径设置为 SSH 协议:

    # 更换为 SSH 协议地址
    $ git remote set-url origin git@github.com:USERNAME/REPOSITORY.git
    
    # 查看远程仓库信息
    $ git remote -v
    > origin  git@github.com:USERNAME/REPOSITORY.git (fetch)
    > origin  git@github.com:USERNAME/REPOSITORY.git (push)
    

更多 Github 配置 SSH 信息,请参考:Connecting to GitHub with SSH

基础操作

使用 Git 对本地仓库进行管理,主要涉及如下命令:

git init

本地对项目进行版本控制,第一步就是初始化 Git 版本控制,使用的是git init命令,其具体格式如下:

git init [-q | --quiet] [--bare] [--template=<template_directory>]
      [--separate-git-dir <git dir>] [--object-format=<format>]
      [-b <branch-name> | --initial-branch=<branch-name>]
      [--shared[=<permissions>]] [directory]

其中:

  • --bare:该选项表示创建一个裸仓库,其特点是不存在工作区,因此裸仓库无法创建或提交修改文件,可认为裸仓库是一个只读仓库。
    通常将裸仓库部署到中央服务器上,作为协同开发的基库,基库无法直接进行修改,但是其他所有结点可以push提交。

    举个例子:通常直接在服务器上使用git init --bare初始化一个裸仓库,然后其他结点clone该裸仓库,进行修改提交,最后pull到该裸仓库即可。这里直接在本地创建一个裸仓库,然后在其他路径克隆该裸仓库,添加一个提交,最后将提交push到裸仓库中:

    1. 本地创建一个裸仓库,名称为rare-repo.git

      $ git init --bare rare-repo.git
      Initialized empty Git repository in /mnt/e/code/temp/learn_git/rare-repo.git/
      
      # 裸仓库内容
      $ ls rare-repo.git
      HEAD  branches  config  description  hooks  info  objects  refs
      

      :裸仓库由于不存在工作区,因此整个仓库就只存储对象数据,也就是普通仓库.git内容,因此,通常会将裸仓库以.git后缀进行命名。

    2. 本地克隆该裸仓库,修改并进行提交:

      $ git clone rare-repo.git rare-repo-temp.git
      Cloning into 'rare-repo-temp.git'...
      warning: You appear to have cloned an empty repository.
      done.
      
      $ cd rare-repo-temp.git
      $ echo 'local: add a commit' > 1.txt
      $ git add 1.txt
      $ git commit -m 'local: add a commit'
      [master (root-commit) fe394b3] local: add a commit
       1 file changed, 1 insertion(+)
       create mode 100644 1.txt
      
      $ git push
      Enumerating objects: 3, done.
      Counting objects: 100% (3/3), done.
      Writing objects: 100% (3/3), 225 bytes | 12.00 KiB/s, done.
      Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
      To /mnt/e/code/temp/learn_git/rare-repo.git
       * [new branch]      master -> master
      
    3. 上一步操作完成后,裸仓库就已经有提交记录了:

      $ cd ../rare-repo.git
      
      $ git log --oneline
      35f0986 (HEAD -> master) local: add a commit
      

      以上,就是裸仓库的一个协同开发过程。

git add

当要将文件纳入版本控制时,首先需要将该文件添加到暂存区中,这样 Git 就会追踪该文件,提交时就会记录该文件当前历史状态。

追踪文件使用命令git add,其语法如下所示:

git add [<options>] [--] <pathspec>...

:如果想一次性添加所有文件到暂存区中,可使用命令:git add .

git commit

如果想保存当前工作区快照,此时可将工作区所有修改进行暂存,然后提交即可。

提交使用的命令为git commit,其语法如下所示:

git commit [<options>] [--] <pathspec>...

git status

我们知道,Git 中存在三个分区:工作区、暂存区和版本库。其中:

  • 工作区:主要用于修改项目源码(即修改文件)。
  • 暂存区:主要用于暂存最新修改。
  • 版本库:当提交时,以暂存区所有被追踪文件为基础,构建出此时完整的项目快照。

由于被追踪文件存在与这三个分区中,因此同一时间,这三个区的同一文件内容可能相同,也可能存在差别。简单来说,依照文件在不同分区之间的差异,主要存在三种状态:已修改(modified)已暂存(staged)已提交(commited)

可通过命令git status来查看文件在这三个区的状态,其语法如下所示:

git status [<options>…] [--] [<pathspec>…]

git status输出的信息相对比较繁杂,此时可使用选项-s, --short来输出更加紧凑的信息:

$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

git log

Git 维护了所有的历史提交信息,可以通过命令git log查看所有提交日志,可以通过git show查看某个提交的具体内容。

git log的语法如下所示:

git log [<options>] [<revision range>] [[--] <path>…]

下面介绍该命令常用的几个选项:

  • 查看分支历史记录

    # 查看 master 分支历史记录
    $ git log master
    
    # 查看 dev 分支历史记录
    $ git log dev
    

    :当未指定分支时,默认查看当前分支历史记录。

  • -n:列举最新的 n 条日志:

    $ git log -n 10
    
    # 也可以直接指定数字
    $ git log -10
    
  • --pretty[=format], --format=<format>:自定义日志显示格式。

    $ git log --date=short --pretty=format:"%h - %an, %ad : %s"
    
    $ git log --format='%h - %an, %ad : %s'
    

    其中,format常用的选项如下表所示:

    选项 说明
    %H commit的完整哈希值
    %h commit的简写哈希值
    %T tree的完整哈希值
    %t tree的简写哈希值
    %P 父提交的完整哈希值
    %p 父提交的简写哈希值
    %an 作者名字
    %ae 作者邮箱
    %ad 作者修改日期(日期可用--date=<format>进行定制
    %ar 作者修改日期(相对时间)
    %cn 提交者名字
    %ce 提交者邮箱
    %cd 提交日期
    %cr 提交日期(相对时间)
    %s 提交描述

    :作者(author)表示实际修改源码的人,提交者(committer)表示最终将修改提交到仓库的人。

  • --oneline:提交的信息以一行进行展示:

    $ git log -n 2 --oneline
    72ffeb997e (HEAD -> master, origin/master, origin/HEAD) Ninth batch
    3d8f81f21b Merge branch 'sa/credential-store-timeout'
    

    --oneline--pretty=oneline --abbrev-commit的简写。

  • -p, --patch:显示每个提交所引入的差异(即当前提交与前一个提交的差异集):

    $ git log -1 --oneline -p
    0e8f570 (HEAD -> master) 555
    diff --git a/2.txt b/2.txt
    new file mode 100644
    index 0000000..c200906
    --- /dev/null
    +++ b/2.txt
    @@ -0,0 +1 @@
    +222
    
  • --stat:简略统计文件变更信息:

    $ git log -n 1 --oneline --stat
    72ffeb997e (HEAD -> master, origin/master, origin/HEAD) Ninth batch
     Documentation/RelNotes/2.30.0.txt | 21 +++++++++++++++++++++ # 2.30.0.txt 文件被修改了,插入了 21 行内容
     1 file changed, 21 insertions(+)
    
  • --name-only:显示更改的文件名:

    $ git log -1 --oneline --name-only
    0e8f570 (HEAD -> master) 555
    2.txt # 最新提交只修改了文件 2.txt
    

    git show结合--name-only可以很方便显示任意一个提交修改的文件集:

    $ git show 0e8f570 --oneline --name-only
    0e8f570 (HEAD -> master) 555
    2.txt
    
  • --graph:展示分支有向无环图(Directed Acyclic Graph,DAG):

    $ git log --oneline --graph
    *   b7846ca (HEAD -> master) Merge branch 'dev'
    |\
    | * c2c7e07 (dev) dev: 222
    * | 4ad81cf master: 333
    |/
    * ce41eeb master: 111
    
  • --author=<pattern>, --committer=<pattern>:过滤显示某个作者/提交者进行的提交(支持正则表达):

    $ git log -n 1 --author=".*Linus.*"
    commit acdd37769de8b0fe37a74bfc0475b63bdc55e9dc
    Author: Linus Torvalds <torvalds@linux-foundation.org>
    
  • --since=<date>, --after=<date>:自从该日期之后:

    # 查询 2020-11-03 17:00:00 之后的所有提交(支持具体日期)
    $ git log --date=local --after='2020-11-03 17:00:00' 
    
    # 一个月前到现在的所有提交(支持相对日期)
    $ git log --date=local --after='1 month ago'  
    

    --date=local可以将日志记录时间转换为本地时间,否则日志上的时间可能以提交者的时区显示。

  • --until=<date>, --before=<date>:在该日期之前:

    # 2005-04-08 之前的提交(包含 2005-04-08)
    $ git log --date=local --before='2005-04-08' 
    
  • --grep=<pattern>:对提交信息(描述)进行匹配(正则匹配):

    $ git log -n 3 --oneline --grep="linus"
    
  • --all-match:过滤满足所有指定的匹配:

    # 匹配同时含有 one 和 two 的提交信息
    $ git log --grep="one" --grep="two" --all-match
    

    :默认多个--author--grep等匹配,执行的是或操作,而--all-match可将之变成与操作。

  • -S <str>:匹配变更文件增加或删除字符串str的提交。该选项在查找特定更改时,十分有用。比如,如果想查看添加或删除某个函数的提交,则使用该选项搜索该函数名即可,如下所示:

    # 显示增加或删除 function_name 的提交
    $ git log -S 'function_name'
    

git rm

Git 中对文件的删除本意是从暂存区中删除该文件,表示不再追踪该文件。

Git 中对删除文件使用命令git rm,其语法如下所示:

git rm [<options>] [--] <file>...

git rm主要有如下两种删除效果:

  • 删除工作区和暂存区文件

    # 工作区文件列表
    $ ls
    1.txt  2.txt
    
    # 查看暂存区文件列表
    $ git ls-files --stage
    100644 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c 0       1.txt
    100644 c200906efd24ec5e783bee7f23b5d7c941b0c12c 0       2.txt
    
    # 删除文件
    $ git rm 1.txt
    rm '1.txt'
    
    # 再次查看工作区,可以看到工作区文件 1.txt 被删除了
    $ ls
    2.txt
    
    # 再次查看暂存区文件列表,可以看到暂存区文件 1.txt 被删除了
    $ git ls-files --stage
    100644 c200906efd24ec5e783bee7f23b5d7c941b0c12c 0       2.txt
    

    上述例子表明直接使用git rm命令会同时删除工作区和暂存区文件。

    :要成功删除工作区文件,前提是工作区文件必须与当前分支版本一致,也即工作区文件不能进行修改,否则无法直接删除成功,此时可通过添加-f参数强制删除该文件。

  • 只删除暂存区文件:表示不再对该文件进行版本控制(不追踪该文件变化):

    # 工作区文件列表
    $ ls
    1.txt  2.txt
    
    # 暂存区文件列表
    $ git ls-files --stage
    100644 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c 0       1.txt
    100644 c200906efd24ec5e783bee7f23b5d7c941b0c12c 0       2.txt
    
    # 删除暂存区文件
    $ git rm --cached 1.txt
    rm '1.txt'
    
    # 工作区文件未被删除
    $ ls
    1.txt  2.txt
    
    # 暂存区文件已被删除
    $ git ls-files -s
    100644 c200906efd24ec5e783bee7f23b5d7c941b0c12c 0       2.txt
    

最后,如果只想删除工作区文件,而不删除暂存区,则使用/bin/rm

提交规范(Conventional Commits specification)

一个结构分明,描述清晰的提交信息对于快速查看、日志过滤等操作是十分有益的,尤其是对于团队项目,一个统一风格的提交信息结构有助于团队成员理解项目变更,防止杂乱信息影响开发进度...具备稳定结构的日志提交,还方便于使用工具自动生成项目变更历史(CHANGELOG.MD)。

当前使用最广泛的提交信息规范为:约定式提交规范(Conventional Commits),它主要受到 Angular规范 的启发。

Conventional Commits 规定,提交信息包含三部分内容:Header、Body 和 Footer,其具体结构如下所示:

<type>[(scope)][!]: <description> # Header
                                  # 空行
[body]                            # Body
                                  # 空行
[footer(s)]                       # Footer

从上述结构可以知道,除了<type>: <description>必须给出外,其他结构都是可选的。
并且任意一行长度不能超过 100 个字符,方便阅读与集成到其他 Git 工具中。

下面介绍各部分具体内容:

Header

Header 总共有三个字段:

  • type:表示提交的类型

    type字段规范如下:

    • type字段 必须 存在

    • type字段 可以 进行自定义,通常其值有如下可选:

      • feat:新增功能(feature)
      • fix:修复漏洞
      • docs:文档更新(documentation)
      • style:格式修改(比如空格、分号、代码格式化等,不影响代码逻辑)
      • refactor:重构
      • perf:性能优化(performance)
      • test:新增测试用例
      • chore:构建流程修改或依赖库/工具更新
      • revert:执行git revert撤销以前的提交时。
        revert时建议为 Footer 添加撤销的提交哈希值,方便后续引用:
      revert: let us never again speak of the noodle incident
      
      Refs: 676104e, a215868
      

      通常会将typefeatfix的提交写入到 Change log。

  • scope:附加说明此次改动波及到的范围、功能、模块等

    scope字段规范如下:

    • scope字段是可选的
    • scope字段 必须 是一个名词(noun)
    • scope字段 必须 由一对小括号包裹起来
    • 当本次提交涉及多个模块时,scope 可以 使用*进行表示
  • description:对此次提交的简短描述

    description字段的规范如下:

    • description字段紧随在type/scope:后面(冒号:后带有一个空格)
    • 应当 第一人称现在时,比如:使用change,而不是changedchanges
    • 首字母 应当 小写
    • 结尾 可以 不加句号.
    • description是对代码更改的简短描述

Body

Body 结构是对本次提交的详细描述

Body 结构的规范如下:

  • Body 结构是可选的
  • 应当 第一人称现在时,比如:使用change,而不是changedchanges
  • Body 信息描述内容没有限制,允许存在多行
  • Body 描述 应当 包含更改动机和与前一个版本的差异比对

Footer

Footer 主要是针对本次提交进行一些备注。主要有两种情况需要进行备注:

  • 不兼容变更:如果此次提交的代码与上一个版本不兼容,则 Footer 部分必须以BREAKING CHANGE开头,或者 Header 中type/scope后使用一个感叹号!强调有重大更改(通常两者都存在,即!BREAKING CHANGE都存在)。一个例子如下所示:

    refactor(http)!: drop support for Node 6
    
    BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.
    
  • 关闭 Issue:如果本次提交解决了某个 Issue,那么可在 Footer 部分备注被关闭的 Issue:

    fix: correct minor typos in code
    
    see the issue for details
    
    Reviewed-by: Z
    Refs #133, #245
    

Footer 结构的规范如下:

  • Footer 结构是可选的
  • 允许存在一个或多个 Footer
  • 每条 Footer 的格式为:<word token>: <str><word token> #<str>
  • Footer 的word token描述如果存在多个词,则每个词 必须-进行连接:<multi-word-token>,这主要是为了能与多行 Body 进行区分。
    :该条规范唯一的例外是BREAKING CHANGE单词描述。
  • Footer 描述允许包含空格和多行内容
  • 对于BREAKING CHANGE必须type/scope后面添加!进行强调,或者在 Footer 中进行声明,声明格式如下:
    BREAKING CHANGE: <description>
    

最后,可以通过使用第三方工具(比如:Commitizen)来确保每次提交信息符合规范。
如果要生成项目变更历史,确保在提交符合规范前提下,可以通过使用第三方工具(比如:conventional-changelog)来自动生成 Change log。

更多提交规范详情,可参考如下文章:

忽略文件

可以通过配置.gitignore来忽略某些文件或文件夹的追踪。

通常只需在根目录配置一个.gitignore文件,该文件规则就可以递归生效到整个仓库中。但是也可以为某些子目录添加.gitignore文件,此时该忽略文件只作用于当前子目录中。

.gitignore的配置规则如下所示:

  • 所有以空行或#开头的字符都会被 Git 忽略。
  • 支持使用标准的 glob 模式匹配(即 Shell 中使用的简化版本的正则表达式),即:
    • 星号*匹配零个或多个字符;
    • 问号?只匹配一个任意字符;
    • [abc]匹配abc中的任意一个字符;
    • [0-9]匹配数字09中的任意一个数字;
    • 两个星号**表示匹配任意层级路径;
  • 以反斜杠/开头表示不进行递归,只作用于当前目录,对子目录无效;
  • 以反斜杠/结尾的表示目录(比如:abc/,则abc为目录);
  • 使用感叹号!表示不忽略文件/文件夹。

一个.gitignore文件例子如下:

# 忽略所有的 .a 文件
*.a

# 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件
!lib.a

# 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO
/TODO

# 忽略任何目录下名为 build 的文件夹
build/

# 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt
doc/*.txt

# 忽略 doc/ 目录及其所有子目录下的 .pdf 文件
doc/**/*.pdf

差异比较

如果想查看同一文件在不同区之间的内容差异,或者查看版本之间的差异,则可以使用git diff命令,其语法如下所示:

git diff [<options>] [<commit>] [--] [<path>...]
git diff [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]
git diff [<options>] [--merge-base] <commit> [<commit>...] <commit> [--] [<path>...]
git diff [<options>] <commit>...<commit> [--] [<path>...]
git diff [<options>] <blob> <blob>
git diff [<options>] --no-index [--] <path> <path>

主要的差异比较有如下几种:

  • 工作区 vs 暂存区:工作区与暂存区文件内容差异:

    # 所有文件差异
    $ git diff 
    
    # 文件 1.txt 在工作区和暂存区内容差异
    $ git diff 1.txt
    diff --git a/1.txt b/1.txt
    index a30a52a..641d574 100644
    --- a/1.txt
    +++ b/1.txt
    @@ -1,2 +1,3 @@
     111
     222
    +333 # 表示工作区 1.txt 比暂存区 1.txt 多了一行内容:333
    

    git diff主语为工作区,因此显示的是工作区相对于暂存区的文件内容改变。

  • 工作区 vs 版本库:工作区与版本库之间文件内容差异:

    # 工作区与版本库最新提交的差异
    $ git diff HEAD
    
    # 1.txt 文件在工作区与版本库倒数第二个提交之间的差异
    $ git diff HEAD~1 1.txt
    
  • 暂存区 vs 版本库:暂存区与版本库之间文件内容差异:

    # 暂存区与版本库最新提交之间的差异
    $ git diff --cached 
    
    # 文件 1.txt 与版本库最新提交的 1.txt 之间的内容差异
    $ git diff --cached HEAD 1.txt
    
  • 版本间的差异

    # HEAD 与 HEAD~1 之间的版本差异(后写的为主语,这里即 HEAD 与 HEAD~1 的内容差别)
    $ git diff HEAD~1 HEAD
    
    # HEAD 版本的文件 1.txt 与 HEAD~1 版本之间的内容差异
    $ git diff HEAD~1 HEAD 1.txt
    

最后,如果觉得git diff输出的比对效果不是很直观,也可以使用命令git difftool来唤起系统默认的比对工具(比如 vimdiff)进行差异比对:

$ git difftool HEAD~1 HEAD
$ git difftool HEAD~1 HEAD 1.txt

回滚操作

Git 操作中,会经常遇到需要进行回滚或撤销动作,常见的回滚操作如下所示:

撤销工作区文件修改

撤销工作区文件修改,恢复到暂存区文件状态,可以使用如下命令:

git restore [--worktree] <file>

工作区文件回退到指定版本

如果想将工作区文件回退到某个提交版本,可以使用如下命令:

git restore [-W | --worktree] {-s | --source}[=]<tree-ish> <file>

git restore默认回退工作区修改,因此可忽略选项-W, --worktree

比如,将文件1.txt重置到当前分支最新提交的版本:

$ git restore --source=HEAD 1.txt

撤销暂存区文件修改

如果对已修改的文件进行了暂存,此时想撤销这个暂存,恢复到版本库最新提交(即HEAD)的文件状态,可以使用如下命令:

git restore {-S | --staged} <file>

暂存区文件回退到指定版本

如果想将暂存区中文件重置到某个版本,可使用如下命令:

git restore {-S | --staged} {-s | --source}[=]<tree-ish> <file>

比如,将文件1.txt重置到第二个最新版本:

$ git restore --staged --source HEAD~1 1.txt

同时回退工作区和暂存区文件到指定版本

结合上面所讲内容,就可以实现同时回退工作区和暂存区文件到某个指定版本,其命令如下:

git restore --worktree --staged --source=<tree-ish> <file>

重置最后一次提交

当一次提交完成后,此时可能出现某些原因导致你想撤销这次提交,则此时可通过命令git commit --amend使用新提交重置上一次提交。

$ git commit -m '3'
$ git add .
# 此时 4 会替换掉 3
$ git commit --amend -m '4'

:如果不需要修改提交信息,可添加--no-edit选项:

$ git commit --amend --no-edit

git commit --amend的效果就相当于使用一个新提交替换掉当前最新的提交(即HEAD),使得原先提交不会出现在历史日志中。其图示如下所示:

git commit --amend

git reset

git reset的主要作用是移动HEAD指针到指定版本,该操作依据携带的选项不同可能会给暂存区和工作区文件带来副作用(重置)。
:当git reset携带一个文件路径时,它起到的作用是重置指定的文件或文件集,但是在 Git 2.23.0 新增了命令git restore,也是用于重置文件,因此与git reset命令功能存在重叠。推荐使用git restore命令重置文件,然后使用reset命令重置仓库版本,这样职责更清晰。

git reset命令语法如下所示:
:我们只介绍git reset重置仓库历史功能。

git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]

其中:

  • --soft:只移动HEAD指针到指定版本,工作区和暂存区内容保持不变。
    :使用--soft选项可以实现git commit --amend功能,只需将HEAD指针移动到第二次最新提交即可,如下所示:
$ git reset --soft HEAD~1
$ git add .
$ git commit -m 'same to --amend'
  • --mixed:移动HEAD指针到指定版本,且暂存区重置到指定版本内容,但是工作区保持不变。
    --mixed选项为git reset命令的默认值。

  • --hard:移动HEAD指针到指定版本,且同时重置暂存区和工作区文件到该指定版本。相当于三个区都重置到指定版本。
    git reset命令中,只有--hard选项使用时需要十分小心,因为只有该选项可能导致文件变更内容无法找回,根源是该选项重置了工作区内容,如果当前工作区被追踪的文件内容存在修改,则此时该修改内容就永久丢失了。
    可以简单这样理解,Git 会尽最大可能保存所有数据记录,但是在涉及到会重置工作区的命令时,存在数据永久丢失风险,因此对工作区重置的命令可认为是危险操作,使用时需多加注意。

git revert

当需要撤销某个提交的修改时,可以使用git revert命令,其语法如下:

git revert [--[no-]edit] [-n] [-m parent-number] [-s] [-S[<keyid>]] <commit>…
git revert (--continue | --skip | --abort | --quit)

git reset的主要作用是移动HEAD指针,可以剔除某些新提交,缩短提交历史。而git revert的作用不是回退提交,而是逆转提交,即逆转某个提交进行的修改,举例来说,比如提交C2增加了文件2.txt,那么git revert C2会删除2.txt,并生成一个新提交,该新提交其实就是逆转C2的操作:

$ git log --oneline
038a3a3 (HEAD -> master) C3
b8568ed C2                       # bad update,need to revert
841b759 C1

$ git show b856 --oneline --stat
b8568ed C2
 2.txt | 1 +                     # C2 增加了文件 2.txt
 1 file changed, 1 insertion(+)

$ ls
1.txt  2.txt                     # 工作区存在 2.txt

$ git revert b856                # 逆转 C2
Removing 2.txt                   # 2.txt 被删除
[master 456796e] Revert "C2"
 1 file changed, 1 deletion(-)
 delete mode 100644 2.txt

$ git log --oneline
5ea9d73 (HEAD -> master) C4: Revert "C2"
038a3a3 C3
b8568ed C2
841b759 C1

$ ls
1.txt                            # 工作区不存在 2.txt

上述代码的图像示意如下:

git revert

:逆转不会对后续版本有任何影响,即git revert C2不会对提交C3有任何影响。

git revert也支持同时逆转多个提交:

# 表示逆转 HEAD~1 和 HEAD
$ git revert -n HEAD~1 HEAD

# 表示逆转 master~5 到 master~2 之间的所有提交(不包含 master~5,但是包含 master~2)
# git revert -n master~5..master~2

git revert可以使用参数-n, --no-commit来禁止自动提交,即此时只会逆转工作区和暂存区内容,而不会生成一个新提交。

当逆转的提交是一个merge commit时,此时需要使用参数-m, --mainline <parent-number>来指定主线分支,主线分支会被保留,而另一条合并分支会被逆转,通常parent-number的取值为12,大多数情况下,1表示当前分支,2表示合并分支(即被合并的分支)。
parent-number具体查找方法请查看:高级技巧 - 查看合并提交的parent-number

可以简单这样理解,merge commit的内容是由两个parent commit合并而成的,两个parent commit代表了两条分支上的改动合集,因此当要逆转这个merge commit时,需要指明逆转哪条分支上的改动,而另一条分支保持不变。

举个例子:假设master分支存在记录C1 <- C2 <- C3 <- C6dev分支存在记录C1 <- C2 <- C4 <- C5 <- C6,如下图所示:

merge commit

由上图可以看出,C3增加了文件3.txtC4添加了文件4.txtC5增加了文件5.txt,因此其合并提交C6就同时增加了文件3.txt4.txt5.txt

如果此时我们逆转C6,由于C6是一个merge commit,因此此时需要使用-m参数来指定主线分支,假设这里主线分支为master,则如下命令会逆转dev分支的修改:

# 转到 master 分支
$ git switch master

# 保留 master 分支(1 表示当前分支),逆转 dev 分支
$ git revert -m 1 HEAD

上述操作结果如下图所示:

git revert merge commit

可以看到,dev分支上的修改被撤销了,此时的新提交C7的内容与C3是完全一样的,从master分支角度上看,此时的效果就好像是从未发生过合并。

如果这时候在dev分支上修补了漏洞,生成一个新提交C8,如下图所示:

fix bug

如果此时合并C7C8,由于C7逆转了C4C5的操作,因此此时合并就不会包含C4C5的内容,导致dev分支合并内容不充分,解决的方法是对C7进行逆转,恢复C4C5的内容,然后在合并C8,如下图所示:

git merge

最后,当进行协同开发时,由于git reset在绝对意义上说只能缩短版本提交记录,这在多人协作场合就不是那么好用了,因为一般服务器共享版本只能增加历史记录,不能进行回退(除非全员有共识,则可以使用git push -f强制进行更新),这种情况下,如果某个提交是“坏”提交,比如某个 bug 是在一个提交上引入的,如果该提交已上传到服务器,则此时可通过git revert来生成一个新的逆转提交,撤销这个 bug,重新引入正确代码即可。

标签

标签的主要作用就是作为对象文件的别名,方便进行引用。通常将标签设置为指向某个提交,作为该提交的别名,后续直接使用该标签就可以查看该提交详情,无需使用提交数字摘要。不过,一般都会将标签以版本号形式打到一些比较重要的提交上,作为版本迭代。

标签的基本操作如下:

创建

Git 中存在两种类型标签:

  • 轻量标签(lightweight):轻量级标签本质是一个引用,其使用方法如下:

    git tag <tag_name> [hash]
    

    例子如下:

    # 默认为当前分支最新提交打上标签 v2.0
    $ git tag v2.0
    
    # 为提交 1a2b3c 打上标签 v1.0
    $ git tag v1.0 1a2b3c
    
  • 附注标签(annotated):附注标签本质是一个对象文件,其使用方法如下:

    git tag -a <tag_name> [commit_id] -m <msg> 
    

    例子如下:

    # 默认为当前分支最新提交打上标签
    $ git tag -a v2.1 -m 'version 2.1'
    
    # 为提交 1a2b3c 打上标签
    $ git tag -a v1.1 1a2b3c -m 'version 1.1'
    

查询

标签的查询主要包含如下几种类型:

  • 列举所有标签:命令如下:

    git tag [-l, --list]
    

    如果需要进行模糊匹配,需要使用-l, --list参数:

    # 列举 v1.8 系列版本
    $ git tag -l "v1.8*"
    
  • 查看具体标签:命令如下:

    git show <tag_name>
    

    比如,查看标签v1.0

    $ git show v1.0
    

删除

删除标签使用如下命令:

git tag -d <tag_name>

比如:

# 删除标签 v1.0
$ git tag -d v1.0

检出标签

如果想查看某个标签源码,可以使用如下命令:

git checkout <tag_name>

此时相当于git reset --hard <tag_name>效果,但是当前仓库是处于游离状态(detached HEAD)的,即HEAD指针指向一个具体的提交(cat .git/HEAD),由于游离状态不处于任何分支中,因此不要在该状态下修改文件,否则可能产生副作用(比如修改无法追踪)。
只需切换回某个分支就可以退出这种状态。

分支

分支是 Git 最强大的特性之一,相比于其他依靠复制副本创建分支的版本控制系统,在 Git 中,创建一个分支是非常轻量级的操作,因为 Git 中,分支的本质就是一个指针,因此无论是创建分支、切换分支还是删除分支等操作,速度都特别快。分支模型是 Git 最受欢迎的一个特性,目前几乎所有团队合作项目,都会基于分支模型进行展开,也因此,各种各样的分支使用规范也应运而生。

先介绍下分支的基本使用:

创建分支

创建一个分支的命令如下所示:

git branch <branch_name>

:创建一个新分支其实就是往一个文件中写入 41 个字节(40 个字符哈希值和 1 个换行符),因此速度特别快。

上述命令会基于当前分支的最新提交创建一个新分支,创建完成后,仍停留在当前分支中。

切换分支

切换分支使用的命令如下:

git switch <branch_name>

:Git 提供了一个简便的方法可以切换回上一个分支:git switch -

查询分支

查询本地仓库中存在的分支,基础的有如下两种用法:

  • 查看本地分支:命令如下:

    # -l 选择支持匹配查找
    git branch [-l, --list]
    

    :该命令列举的分支位于目录.git/refs/heads

  • 列举本地所有分支:命令如下:

    git branch -a
    

    该命令会同时列举本地仓库分支和已追踪的远程分支。

  • 查询已合并的分支

    # 查看已被 main_branch 合并的分支,main_branch 未设置则默认为当前分支
    git branch --merged [main_branch]
    

    :该命令不会列举已合并但被删除的分支。

  • 查询未合并的分支

    # 查看未被 main_branch 合并的分支,main_branch 未设置则默认为当前分支
    git branch --no-merged [main_branch]
    

创建并切换分支

如果想创建一个分支并同时切换到新创建的分支中,可以使用如下命令:

git switch [<options>] (-c|-C) <new-branch> [<start-point>]

-c对应的长选项为--create,表示创建分支,而git switch可用于切换分支,两者结合,就能实现切换到新创建分支中。
start-point是基准分支,即基于start-point分支上创建新分支new-branch,当start-point未指定时,默认基于当前分支。

一个需要注意的点是,每次切换分支时,工作区和暂存区都会被重置到切换分支最新提交状态,因此,切换分支前必须确保当前工作目录干净(即git commitgit stash),否则无法切换成功,因为 Git 会尽最大能力确保不丢失用户任何修改。

:默认情况下,创建一个新分支,新分支内容会基于当前分支,也即新分支包含当前分支最新提交及其之前所有提交历史,有时候可能需要创建一个空分支,也即一个不包含任何提交记录的全新分支,那么可以使用如下命令:

git switch [<options>] --orphan <new-branch>

举个例子:比如创建一个空分支rare

# 当前分支存在提交
$ git log --oneline -1
7574f83 (HEAD -> master) feat: 111

# 创建并切换到空分支 rare
$ git switch --orphan rare
Switched to a new branch 'rare'

# 空分支不包含任何提交记录
$ git log
fatal: your current branch 'rare' does not have any commits yet

重命名分支

重命名或移动分支命令如下所示:

git branch [<options>] (-m | -M) [<old-branch>] <new-branch>

其中:-m, --move表示移动或重命名分支,如果本地仓库存在同名分支,则需要使用选项-M进行强制重命名。

:如果未指定old-branch,则默认对当前分支进行修改。

删除分支

删除分支需要使用选项-d, --delete,具体如下所示:

git branch -d <branch_name>

-d, --delete只能删除已合并的分支,如果分支未合并,则只能强制进行删除:

git branch -D <branch_name>

合并分支

分支合并使用的命令为git merge,其语法具体如下所示:

git merge [<options>] [<commit>...]
git merge --abort
git merge --continue

分支合并最常见的场景就是将两条分支的变动合并到一起,生成一个新的合并提交。分支合并通常使用三路合并策略,即将当前分支最新提交、合并分支最新提交和两分支的最近公共祖先提交进行一个合并操作,以最近公共祖先提交作为基准点,其他分支上的提交与基准点不同的变动会被进行合并,如果两条分支对同一文件的同一处地方都进行了变动,这时就会产生冲突,需要手动解决该冲突,然后继续恢复合并过程即可,这也就是分支合并的基本原理。

下面以一个例子来驱动介绍分支合并相关内容:

  1. 假设本地仓库当前存在提交C1C2

    $ git init demo_merge && cd demo_merge
    Initialized empty Git repository in /mnt/e/code/temp/learn_git/demo_merge/.git/
    
    $ echo 'C1' > 1.txt
    $ git add . && git commit -m 'C1'
    [master (root-commit) 6210074] C1
     1 file changed, 1 insertion(+)
     create mode 100644 1.txt
    
    $ echo 'C2' > 2.txt
    $ git add . && git commit -m 'C2'
    [master fbca408] C2
     1 file changed, 1 insertion(+)
     create mode 100644 2.txt
    
    $ git log --oneline --graph
    * fbca408 (HEAD -> master) C2
    * 6210074 C1
    

    此时的示意图如下所示:

    master: C1 <- C2
  2. 此时在master分支上创建一个分支dev,然后为dev创建两个新提交:C3C4

    $ git switch -c dev
    Switched to a new branch 'dev'
    
    $ echo 'C3' > 3.txt
    $ git add . && git commit -m 'C3'
    [dev da5704c] C3
     1 file changed, 1 insertion(+)
     create mode 100644 3.txt
    
    $ echo 'C4' > 4.txt
    $ git add . && git commit -m 'C4'
    [dev 2db1365] C4
     1 file changed, 1 insertion(+)
     create mode 100644 4.txt
    
    $ git log --oneline --graph
    * 2db1365 (HEAD -> dev) C4
    * da5704c C3
    * fbca408 (master) C2
    * 6210074 C1
    

    此时的示意图如下所示:

    dev: C3 <- C4
  3. 此时切换到master,合并dev分支:

    $ git switch master
    Switched to branch 'master'
    
    $ git merge dev
    Updating fbca408..2db1365
    Fast-forward                # 使用快进模式
     3.txt | 1 +
     4.txt | 1 +
     2 files changed, 2 insertions(+)
     create mode 100644 3.txt
     create mode 100644 4.txt
    

    从合并结果消息可以看到,本次合并采用了Fast-forward模式,即快进模式,该模式其实就是直接将master指针移动到C4上,因为dev分支上的合并结点C4master分支虽然处于两个不同的分支中,但是位于同一条时间线上,合并的两个结点中,C2C4的直接祖先,所以合并这两者的时候,直接将master指针向前移动就可以了。

    此时的示意图如下所示:

    master: merge fast-forward

    从上述示意图可以清晰看到,Fast-forward模式合并的效率非常高,因为就只是移动了指针而已,但是这种模式的缺点是会丢失分支合并信息,因为没有构造出有向无环图(DAG,directed acyclic graph),此时如果查看历史记录,完全就是单分支记录:

    $ git log --oneline --graph
    * 2db1365 (HEAD -> master, dev) C4
    * da5704c C3
    * fbca408 C2
    * 6210074 C1
    
  4. 如果想保留分支合并信息,那么就需要禁用Fast-forward模式,可通过选项--no-ff来禁止该模式。

    下面介绍禁用Fast-forward模式来合并分支的效果:

    1). 首先将master分支重置到C2状态:

    $ git reset --hard HEAD~2
    HEAD is now at fbca408 C2
    
    $ git log --oneline --graph
    * fbca408 (HEAD -> master) C2
    * 6210074 C1
    

    此时的示意图如下所示:

    master: reset to C2

    2). 然后使用--no-ff合并dev分支:

    $ git merge --no-ff dev -m 'C5: merge with --no-ff'
    Merge made by the 'recursive' strategy.
     3.txt | 1 +
     4.txt | 1 +
     2 files changed, 2 insertions(+)
     create mode 100644 3.txt
     create mode 100644 4.txt
    

    3). 查看此时日志记录:

    $ git log --oneline --graph
    *   65a1d2f (HEAD -> master) C5: merge with --no-ff
    |\
    | * 2db1365 (dev) C4
    | * da5704c C3
    |/
    * fbca408 C2
    * 6210074 C1
    

    可以看到,DAG 图出现了,因为--no-ff会生成一个新提交C5来合并C2C4,这样分支合并就构成有向无环图,分支合并信息就得以保存了。

    此时的示意图如下所示:

    master: merge --no-ff

    :除非明确操作目的,否则建议合并分支时始终使用--no-ff来禁止Fast-forward模式,保留分支合并信息。

  5. 有时候合并分支时,会出现冲突现象。冲突的本质原因是不同的分支对同一个文件相同的位置进行了不同的修改,这样在合并的时候,Git 无法确定保存哪条分支上的修改,于是暂停合并,抛出冲突,交由开发者手动解决该冲突,然后再继续合并过程。

    下面介绍下合并冲突处理流程:
    1). 首先将上述例子master分支重置到C2

    $ git reset --hard fbca
    HEAD is now at fbca408 C2
    
    $ git log --oneline --graph
    * fbca408 (HEAD -> master) C2
    * 6210074 C1
    

    此时的示意图如下所示:

    master: reset to C2

    2). 现在在master分支上创建文件4.txt,并写入数据,进行提交:

    $ echo 'master: C5' > 4.txt
    $ git add 4.txt
    $ git commit -m 'C5'
    [master 94e6038] C5
     1 file changed, 1 insertion(+)
     create mode 100644 4.txt
    

    此时的示意图如下所示:

    master: C5

    3). 此时合并masterdev分支:
    :由于此时master分支所在提交(即C4)不是dev分支所在提交(即C5)的直接祖先,因此合并不是使用Fast-forward模式,而是会将两个分支的末端提交点(即C4C5)以及这两个分支的公共祖先提交(即C2)进行一个简单的三路合并(simple three-way merge)。

    $ git merge dev -m 'C6: simple three-way merge'
    CONFLICT (add/add): Merge conflict in 4.txt     # 4.txt 文件有冲突
    Auto-merging 4.txt
    Automatic merge failed; fix conflicts and then commit the result.
    

    不出意外,此时出现了合并冲突,从输出中可以看到,4.txt文件产生了冲突,原因是C4C5同时对该文件相同位置进行了修改。

    :此时也可以通过git status命令来查看哪些文件产生了冲突:

    $ git status
    On branch master
    You have unmerged paths.
      (fix conflicts and run "git commit")
      (use "git merge --abort" to abort the merge)
    
    Changes to be committed:
            new file:   3.txt
    
    Unmerged paths:                                # 未合并路径
      (use "git add <file>..." to mark resolution)
            both added:      4.txt                 # 冲突文件
    

    4). 此时查看冲突文件4.txt内容:

    $ cat 4.txt
    <<<<<<< HEAD
    master: C5
    =======
    C4
    >>>>>>> dev
    

    可以看到,冲突位置由符号<<<<<<<=======>>>>>>>包裹起来,其中:

    • <<<===之间是主分支的内容。
    • ===>>>之间是合并分支的内容。

    5). 我们可以手动修改该冲突文件,或者使用git mergetool唤起默认冲突比对工具进行修改,修改完成后,还需要将修改完的文件添加到暂存区,然后恢复合并过程:

    $ echo -e 'master: C5\nC4' > 4.txt          # 手动解决冲突
    $ cat 4.txt
    master: C5
    C4
    
    $ git add 4.txt                             # 冲突修改完成,添加文件到暂存区
    
    $ git merge --continue                      # 继续合并过程
    [master 95a28cd] C6: simple three-way merge
    

    合并完成后,查看日志记录:

    $ git log --oneline --graph
    *   95a28cd (HEAD -> master) C6: simple three-way merge
    |\
    | * 2db1365 (dev) C4
    | * da5704c C3
    * | 94e6038 C5
    |/
    * fbca408 C2
    * 6210074 C1
    

    可以看到,三路合并最终生成了一个新提交。

    此时的示意图如下所示:

    master: simple three-way merge

分支规范

分支模型是 Git 的杀手级特性,在团队项目中,分支的使用十分广泛,但是由于分支十分灵活,因此在团队协作时,最好遵循一套标准分支管理规范,方便代码维护与管理,有利于项目持续迭代。

目前主流的分支管理采用多路并发协作模型,具体来说,一个仓库中,主要存在如下几种功能分支:

  • master分支:作为主分支,仅用于发布应用新版本。

    共享仓库(比如远程仓库)默认创建master分支,且具备特殊权限,即只有开发主管具备上传权限,普通开发者无法推送代码到master分支上。

    master分支必须十分稳定,任何时候不能直接在master分支上进行代码开发。

    master分支只能通过合并其他分支来进行版本更新,通常合并dev分支(实际是合并release分支)来更新到最新发布版本,合并hotfix分支来紧急修补线上漏洞。

    每次合并的代码都必须经过严密的测试,确保能达到发布时才合并到master分支中。

    所有在master分支上的提交都要打上对应的版本标签,记录版本迭代过程。

  • dev分支:即开发(develop)分支,主要用于开发新功能。

    共享仓库(比如远程仓库)默认创建dev分支,团队成员统一基于dev分支进行开发,所有成员皆具备推送更新到dev分支。

    dev分支保持在最新功能迭代状态,紧急 bug 修复后,需要同时合并到dev分支中。

    对于小功能或小漏洞修补,可直接在dev分支上进行;对于比较独立的功能特性,一般基于dev分支创建feature分支,在feature分支上开发新功能特性,完成后再合并到dev分支上。

  • feature分支:特性分支,开发独立模块,或者独立功能时,以dev分支为基础,创建特性分支。

    feature分支为本地分支,各开发者依据自己模块独立进行开发。

    依据新模块或新功能特性,以feature/xxx形式进行命名,比如:feature/order_modulefeature/network...

  • issue分支:即漏洞修复分支。用于普通漏洞修复。

    issue分支为本地分支,各开发人员依据各自负责功能模块的漏洞新开一个相应分支进行修复。

    issue分支依据漏洞号进行命名,比如:issue-101...

  • hotfix分支:当发布版本或线上出现紧急漏洞时,新开hotfix分支进行修复。

    由于是线上版本出现问题,因此hotfix分支是基于master分支之上进行创建。

    hotfix分支的命名规则与feature分支类似,以问题作为分支名,形为:hotfix/xxx

    hotfix分支修复完成后,需要同时合并到master分支和dev分支上。

  • release分支:预发布阶段,新开release分支进行测试。

    各个团队开发者会及时将各自已完成的feature分支合并到dev分支,当一组feature开发完成时,此时可以进入发布测试阶段。

    发布测试会以dev分支为基准,创建release分支,在该release分支上进行发布测试。

    当测试出现问题时,由开发者直接在这个release分支上进行漏洞修复,并进行提交。

    当测试完成后,合并release分支到dev分支和master分支上,此时master分支的最新提交就可以作为新版本正式发布。

最后,除了masterdev分支外,其余分支都是临时性分支,也即在合并完后,就立即进行删除。

当然,每个团队都有不同的 git-flow 流程,但大致与上述介绍的差别不大。更多详细内容,可参考如下文章:

储藏

前面说过,在进行分支切换时,必须确保当前工作目录干净,这样分支才能成功切换,但在实际开发中,很可能我们还处于新特性开发过程中,就突然接到一个临时紧急任务需要开发,此时当然是新开一个分支来处理该紧急任务,但是由于当前分支工作目录不干净,无法直接进行分支创建与切换,这时就可以借助git stash功能。

git stash提供了储藏功能,它本质是对分支当前暂存区和工作区目录进行备份,也即储藏当前修改的内容,然后恢复到分支当前最新提交状态,这样分支当前工作目录就处于干净状态,这时候就可进行分支创建与切换了,后续切换回该分支时,再将储藏的修改进行恢复,就可以继续之前未完成的开发了。

储藏涉及的操作主要有如下:

开启储藏

要对当前状态进行储藏,其命令如下:

git stash [push]

git stash默认只储藏被追踪文件,如果想存储未被追踪的文件,可使用-u, --include-untracked参数,如果想存储所有文件(包括.gitignore忽略的文件),可以使用-a, --all参数。

git stash的实现原理其实就是对当前状态进行了一个提交,而且是一个合并提交,简单来说,如果直接运行git stash,那么 Git 会首先依据当前工作目录和暂存区状态,生成一个提交(假设为C2),然后将该提交与当前分支最新提交(假设为C1)进行合并,生成储藏提交。而如果加上-u选项,则 Git 还会对未被追踪的文件进行提交,生成一个提交对象(假设为C3),然后C1C2C3进行一个简单三路合并,生成一个储藏提交对象。
然后.git/refs/stash引用文件指向这个生成的储藏对象文件,多个储藏会生成多个commit对象文件,.git/refs/stash始终指向最新的储藏commit对象,日志文件.git/logs/refs/stash记录了每次stash对应生成的commit对象,当git stash pop时候,从该日志文件找到前一个stash对应的commit对象,设置到.git/refs/stash文件即可,这样就完成了储藏与恢复功能。示意图如下所示:

git stash implementation

:以上git stash原理实现只是本人猜测,并未进行细究,可能与真正实现不符,但本人觉得应该差不多是这个原理。

查询储藏

  • 列举所有储藏
$ git stash list
stash@{0}: WIP on master: 96c3a4e 111
stash@{1}: WIP on master: 96c3a4e 111
  • 查看具体储藏:储藏本质是一个commit,所以可直接使用git show进行查看:
$ git show stash@{0}
commit 7d0d67b196c6c90421dfcd3e17099163a3199839 (refs/stash)
Merge: 96c3a4e 17afeb3 a94cafa
Author: Why8n <Why8n@gmail.com>
Date:   Sun Dec 27 10:57:59 2020 +0800

    WIP on master: 96c3a4e 111

恢复储藏

当需要回到当前分支,恢复先前储藏的修改时,可以使用如下命令:

git stash apply [<stash>]

stash表示储藏的名称或索引(即stash@{0}中的0),未指定时则默认恢复最后一次储藏。
git stash apply恢复现场后,不会删除该储藏。

删除储藏

删除储藏使用如下命令:

git stash drop [<stash>]

恢复并删除储藏

如果想实现恢复后,直接删除储藏,可以使用如下命令:

git stash pop [<stash>]

远程操作

我们可以将仓库托管到远程服务器上,这样全世界所有人都可以基于该远程仓库开展协同工作。

远程仓库最终还是需要先下载到我们本地进行开发,然后再将完成的新特性上传到服务器上。这期间的操作涵盖本地和远程及其两者之间的互操作,因此我将 Git 的远程操作划分为如下几块:

  • 远程仓库设置:主要针对远程仓库的一些设置,方便本地查看与引用远程仓库相关信息。
  • 本地操作:主要就是对远程仓库的一些本地操作,主要包含源码下载与上传,分支关联等操作。
  • 远程分支:主要涉及对远程仓库分支的一些操作,包含远程分支下载与删除等操作。
  • 远程标签:主要涉及对远程仓库标签的一些操作,包含本地标签的上传与远程仓库标签的删除等操作。

远程仓库设置

如果想要下载远程仓库到本地,那么本地目录首先必须关联到远程仓库,这样才能进行下载以及执行后续的一些联合操作...

本地关联远程仓库主要涉及如下一些操作与配置:

链接远程仓库

远程仓库通常存在于网络上,因此首先在本地第一步操作就是要链接该远程仓库,其实就是将远程仓库添加到本地 Git 仓库中,其语法如下所示:

git remote add [-t <branch>] [-m <master>] [-f] [--tags | --no-tags] [--mirror=<fetch|push>] <name> <url>

:一个仓库可以容纳多个远程仓库。

主要就是使用git remote add命令来为本地 Git 仓库添加一个远程仓库,也可以认为是为远程仓库设置了一个别名,让我们能通过别名引用该远程仓库,而无需使用远程仓库路径。

举个例子:比如,假设本地存在一个 Git 仓库config_git_remote,我们想将其关联到远程仓库https://github.com/Why8n/learn_git中,则只需执行如下操作:

# 创建并初始化本地 Git 仓库
$ git init config_git_remote && cd config_git_remote
Initialized empty Git repository in /mnt/e/code/temp/learn_git/config_git_remote/.git/

# 本地 Git 仓库链目标接远程仓库,以 origin 指代远程仓库
$ git remote add origin 'https://github.com/Why8n/learn_git'

# 查看远程仓库信息,可以看到,设置成功了
$ git remote -v
origin  https://github.com/Why8n/learn_git (fetch)
origin  https://github.com/Why8n/learn_git (push)

:其实此时就可以执行git pull操作了,且可以成功下载源码,但由于本地仓库未设置相关分支,因此下载的源码不会进行合并。

重置远程仓库地址

可以通过命令git remote set-url来重置远程仓库地址,其语法如下所示:

git remote set-url [--push] <name> <newurl> [<oldurl>]
git remote set-url --add <name> <newurl>
git remote set-url --delete <name> <url>

举个例子:仍用上述例子,前面我们设置的远程仓库地址是HTTP的,由于我们本地与服务器已配置了 SSH,将仓库更改为 SSH 通讯会更加方便:

# 重置远程仓库地址
$ git remote set-url origin 'git@github.com:Why8n/learn_git.git'

# 查看远程仓库地址
$ git remote get-url origin
git@github.com:Why8n/learn_git.git

git remote add用于增加新远程仓库,仓库是从无到有的过程,而git remote set-url用于更新仓库地址,仓库更新前必须存在:

# 不能增加已存在的远程仓库
$ git remote add origin 'https://github.com/Why8n/learn_git.git'
error: remote origin already exists.

# 不能更新不存在的远程仓库
$ git remote set-url remote_not_exists 'https://github.com/Why8n/learn_git.git'
error: No such remote 'remote_not_exists'

远程仓库查询

当成功链接远程仓库后,就可以查询远程仓库的一些信息:

  • 查看远程粗略信息:主要就是查看仓库名及其地址,使用如下命令即可:

    $ git remote -v
    origin  git@github.com:Why8n/learn_git.git (fetch)
    origin  git@github.com:Why8n/learn_git.git (push)
    
  • 查看远程仓库信息:显示远程仓库较详细信息,包含别名,地址,活动分支,远程分支等信息:

    $ git remote show origin
    * remote origin                                           # 别名
      Fetch URL: git@github.com:Why8n/learn_git.git           # 地址
      Push  URL: git@github.com:Why8n/learn_git.git
      HEAD branch: master                                     # 活动分支
      Remote branches:                                        # 远程分支信息
        develop new (next fetch will store in remotes/origin)
        master  new (next fetch will store in remotes/origin)
    
  • 列举远程仓库对象文件信息:使用git ls-remote命令可以查询远程仓库的对象文件信息,包括远程分支、远程标签等信息。其语法如下所示:

    git ls-remote [--heads] [--tags] [--refs] [--upload-pack=<exec>]
                         [-q | --quiet] [--exit-code] [--get-url]
                         [--symref] [<repository> [<refs>...]]
    

    git ls-remote常见用法如下所示:

    • 列举远程仓库所有引用:下面命令列举了远程仓库origin所有分支和标签等对象模型信息:

      $ git ls-remote origin
      b51743fac29d8a99557c560bb517971eb77a2725        HEAD
      b51743fac29d8a99557c560bb517971eb77a2725        refs/heads/master
      b51743fac29d8a99557c560bb517971eb77a2725        refs/tags/v1.0
      

      git ls-remote即使在没有clonefetch情况下,甚至于在为配置远程仓库信息时,仍可进行查询,只需提供远程仓库地址即可:

      $ git ls-remote 'git@github.com:Why8n/learn_git.git'
      b51743fac29d8a99557c560bb517971eb77a2725        HEAD
      b51743fac29d8a99557c560bb517971eb77a2725        refs/heads/master
      b51743fac29d8a99557c560bb517971eb77a2725        refs/tags/v1.0
      
    • 列举远程仓库所有分支:如果只需查询远程仓库所有分支信息,可以添加选项-h, --heads

      $ git ls-remote --heads origin
      b51743fac29d8a99557c560bb517971eb77a2725        refs/heads/master
      
    • 列举远程仓库所有标签:如果只需查询远程仓库所有标签,可以使用选项-t, --tags

      $ git ls-remote --tags origin
      b51743fac29d8a99557c560bb517971eb77a2725        refs/tags/v1.0    # 轻量级标签
      ad6b5db111ab7f56db148f78555f3eb61ac80f41        refs/tags/v0.2^{} # ^{} 表示 v0.2 是附注标签
      

重命名远程仓库

其实就是为远程仓库更换一个别名,使用的命令如下所示:

git remote rename <old> <new>

git remote set-url用于重置远程仓库地址,git remote rename用于重置远程仓库别名。

举个例子:将远程仓库别名origin更改为remote_repo01

$ git remote rename origin remote_repo01

$ git remote -v
remote_repo01   git@github.com:Why8n/learn_git.git (fetch)
remote_repo01   git@github.com:Why8n/learn_git.git (push)

删除远程仓库

如果想要删除某个远程仓库,使用如下命令即可:

git remote remove <name>

举个例子:比如删除远程仓库origin

# 手动添加一个新远程仓库,名称为 origin
$ git remote add origin 'git@github.com:Why8n/learn_git.git'

# 查看所有的远程仓库
$ git remote -v
origin  git@github.com:Why8n/learn_git.git (fetch)
origin  git@github.com:Why8n/learn_git.git (push)
remote_repo01   git@github.com:Why8n/learn_git.git (fetch)
remote_repo01   git@github.com:Why8n/learn_git.git (push)

# 删除远程仓库 origin
$ git remote remove origin

# 删除成功
$ git remote -v
remote_repo01   git@github.com:Why8n/learn_git.git (fetch)
remote_repo01   git@github.com:Why8n/learn_git.git (push)

本地操作

本地对远程仓库的操作主要涉及以下内容:

源码下载

前面我们都是通过手动配置远程仓库信息,之后才能进行源码下载,其实,Git 提供了一个简便的命令可以让我们直接下载源码仓库,并自动配置相关信息。这个命令就是git clone,其语法如下所示:

git clone [<options>] [--] <repo> [<dir>]

git clone会下载源码文件到指定目录中,同时会默认做以下几件事:

  • 自动配置远程仓库信息git clone下载源码时,会默认创建一个远程仓库origin,指向下载的地址。
  • 自动配置追踪分支git clone自动创建远程仓库的所有分支追踪(可通过git branch {-r | --remotes}查看)。
  • 默认检出活动分支git clone在下载完源码后,会自动检出活动分支到工作目录(活动分支由HEAD指针决定,通常默认的活动分支为master)。

由于git clone下载远程仓库的同时,也设置了追踪分支,因此当直接使用git fetch时,所有的远程分支都会被拉取到本地,当使用git pull时,除了拉取分支外,还会将拉取到的远程master分支合并到本地master分支中(一个例外情况就是使用了--single-branch)。

拉取分支更新

要拉取远程仓库的更新内容,可以使用命令git fetch,其语法如下所示:

git fetch [<options>] [<repository> [<refspec>...]]
git fetch [<options>] <group>
git fetch --multiple [<options>] [(<repository> | <group>)...]
git fetch --all [<options>]

简单来说,git fetch主要有如下几种常用操作:

  • 默认下载origin所有分支:当使用不带参数的git fetch命令时,默认下载远程仓库origin所有被追踪的分支。

  • 只拉取指定分支:如果只想拉取远程仓库的指定分支,则只需指定分支名即可,其格式如下所示:

    git fetch <remote> <branch_name>
    

    举个例子:比如,如果只想拉取远程仓库originmaster分支,则命令如下:

    $ git fetch origin master
    

    上述代码通过git fetch将远程仓库master分支最新代码拉取到本地,但是这些代码不会污染工作区(即工作区内容保持不变),拉取的代码存储在本地远程分支origin/master上,可通过该分支查看更新的内容,也可以将本地当前源码与拉取的源码进行比对,查看差异:

    # 查看远程仓库 master 分支历史记录
    $ git log origin/master
    
    # 查看远程仓库 master 分支最新提交详情
    $ git show origin/master
    
    # 比对当前分支与远程仓库 master 分支最新版本的差异
    $ git diff HEAD origin/master
    

分支合并

通过git fetch拉取的更新,可以通过git merge合并到本地对应追踪分支上。其语法如下所示:

git merge [<options>] [<commit>...]
git merge --abort
git merge --continue

举个例子:比如想为当前分支合并远程分支origin/master,命令如下所示:

$ git merge origin/master

:如果未指定要进行合并的远程分支(比如,上述命令省略origin/master),则默认合并当前分支追踪的远程分支。

:另一种很常用的分支合并方式是git rebase,具体详情请查看后文:高级技巧 - git rebase

合并更新

git pull其实就是git fetchgit merge的组合命令,可以一步到位完成分支拉取与合并。其语法如下所示:

git pull [<options>] [<remote> <remote_branch>:<local_branch>]

其中,一些常用的options有:

  • -r, --rebasegit pull默认采用git merge进行分支合并,如果想使用rebase模式,可使用选项-r, --rebase
  • -p, --prune:当远程仓库删除了某个分支时,git pull不会在拉取分支的时候,删除对应的本地分支,避免数据丢失,但是如果确实想删除这些本地分支,则可使用-p, --prune选项。

:当git pull未指定参数时,默认会下载所有远程分支,并将当前分支与其追踪的远程分支进行合并。
:当未指定local_branch时,默认将远程分支remote_branch与本地当前分支进行合并。

举个例子:假设本地存在masterdev分支,且master分支追踪远程分支origin/master

# 切换到本地 master 分支
$ git switch master 

# 下载所有远程分支,并且将`origin/master`合并到当前本地分支上
$ git pull

# 下载远程分支`origin/develop`,合并到本地分支`master`上
$ git pull origin develop:master

# 由于当前处于 master 分支上,因此上面命令可简化为如下
$ git pull origin develop

推送分支

当本地仓库进行了一些修改后,如果想将修改上传到远程仓库中,可以使用命令git push进行推送。其语法如下所示:

git push [<remote> <local_branch>:<remote_branch>]

可以看到,git pushgit pull的格式差不多,但是两者local_branchremote_branch位置相反,虽然看似两者的结构不同,但其实是一致的,只要将代码来源作为基准,其统一的结构就为:<代码来源>:<代码去处>,git push是将本地代码上传到服务器,因此是<local_branch>:<remote_branch>,而git pull是从服务器下载源码到本地,因此是<remote_branch>:<local_branch>

简单来说,git push主要有如下几点需要注意:

  • 只推送当前分支:如果命令git push为携带任何参数,则默认只推送当前分支:

    # 默认推送当前分支到默认远程仓库(通常为 origin)上
    $ git push
    
    # 默认推送当前分支到 origin 远程仓库上
    $ git push origin
    
  • 推送特定分支:如下所示,将本地dev分支推送到远程仓库develop分支上:

    $ git push origin dev:develop
    

    :单纯推送分支并不会建立追踪关系,此时可通过添加-u, --set-upstream选项在上传的同时设置追踪关系:

    $ git push -u origin dev:develop
    
  • 未指定remote_branch:如果省略了remote_branch,则默认推送到同名远程分支,如果不存在同名远程分支,则为远程仓库创建该分支(但此时该远程新分支与本地分支不具备追踪关系):

    # 上传本地分支 dev 到远程同名分支
    $ git push origin dev
    
  • 未指定local_branch:如果省略了local_branch,则相当于删除远程分支remote_branch

    # 删除远程分支 dev
    $ git push origin :dev
    
  • 推送所有分支:如果想推送本地所有分支到服务器,只需使用--all选项即可:

    $ git push origin --all
    

建立追踪关系

前面说过,使用git clone命令下载的远程仓库,会自动对远程分支进行追踪。我们也可以手动对本地仓库各分支建立追踪关系。

手动建立分支追踪关系大致有如下三种方法:

  • 手动指定:通过使用git branch --set-upstream-to可手动建立分支追踪关系。其具体格式如下所示:

    git branch (--set-upstream-to=<remote/branch_name> | -u <remote/branch_name>) [<local_branch>]
    

    :如果未指定local_branch,则默认为当前分支设置远程分支追踪。

    举个例子:比如设置本地分支dev追踪远程分支origin/dev

    # 首先确保源码分支 origin/dev 已下载到本地
    $ git fetch origin dev
    From github.com:Why8n/learn_git
     * branch            dev        -> FETCH_HEAD
    
    # 创建本地分支 dev
    $ git branch dev
    
    # 设置本地分支 dev 追踪远程分支 origin/dev
    $ git branch --set-upstream-to=origin/dev dev
    Branch 'dev' set up to track remote branch 'dev' from 'origin'.
    
    # 查看分支追踪信息
    $ git branch -vv
      dev    b51743f [origin/dev] Merge branch 'test3' of github.com:Why8n/learn_git
    * master b51743f [origin/master] Merge branch 'test3' of github.com:Why8n/learn_git
      test01 b51743f Merge branch 'test3' of github.com:Why8n/learn_git
    
  • 创建分支时指定追踪关系:当创建新分支时,可直接在分支后面指定追踪的远程分支:

    # 分支创建时指定追踪分支
    $ git branch dev origin/dev
    Branch 'dev' set up to track remote branch 'dev' from 'origin'.
    
    # 分支创建并切换时指定追踪分支
    $ git switch -c dev origin/dev
    Branch 'dev' set up to track remote branch 'dev' from 'origin'.
    Switched to a new branch 'dev'
    
  • push时建立追踪关系:可直接在git push的时候指定分支追踪关系:

    $ git push -u origin dev
    

取消分支追踪

取消分支追踪只需使用--unset-upstream选项即可,其格式如下所示:

git branch --unset-upstream [<local_branch>]

:当local_branch未指定时,默认取消当前分支的追踪关系。

举个例子:比如取消dev分支追踪关系:

$ git branch --unset-upstream dev

远程分支

对远程分支的操作,主要涉及如下:

下载/检出远程分支

使用git clone下载远程仓库时,虽然会下载所有分支源码,但默认只检出活动分支。当然由于所有远程分支都已下载到本地,我们也可以检出其他分支源码到工作目录中,只需使用checkout命令即可:

# 检出 origin/develop 分支到工作目录
$ git checkout origin/develop

# 注意:此时 Git 处于游离态
$ git branch
* (HEAD detached at origin/develop)
  dev
  master

# 游离态本质是 HEAD 指针指向了一个具体提交,而不是分支
$ cat .git/HEAD
b51743fac29d8a99557c560bb517971eb77a2725

从上述操作可以看到,虽然我们在本地检出远程分支,但却会让 Git 处于游离态,这种操作适用于查看分支代码,但不适用于对代码进行修改,因为游离态修改代码存在丢失等风险。因此,通常如果我们对某个远程分支感兴趣,或者需要修改该远程分支源码,比较好的做法是将该远程分支下载合并到本地对应追踪分支上,效果其实就相当于本地下载了远程分支。

简单来说,下载远程分支或者说本地激活远程分支大致有如下几种方法:

  • 下载源码时指定分支:下载源码时,可以直接指定检出分支,而不是默认检出master分支:
    # 通过 -b 选项指定检出分支
    $ git clone 'git@github.com:Why8n/learn_git.git' -b dev
    Cloning into 'learn_git'...
    remote: Enumerating objects: 26, done.
    remote: Counting objects: 100% (26/26), done.
    remote: Compressing objects: 100% (13/13), done.
    remote: Total 26 (delta 7), reused 17 (delta 4), pack-reused 0
    Receiving objects: 100% (26/26), done.
    Resolving deltas: 100% (7/7), done.
    
    $ cd learn_git
    
    # 当前活动分支为 dev
    $ git branch
    * dev
    
    :使用-b选项只是指定默认检出远程分支到本地同名分支(自动创建本地同名分支并建立远程分支联系),但实际上整个远程仓库所有分支都下载了下来,手动创建其他分支时,会自动合并同名远程分支。
  • 将源码检出到新分支中:本地可以创建一个新分支,然后将源码检出到新分支中(相当于本地分支设置远程追踪分支):

    $ git switch -c dev origin/dev
    Branch 'dev' set up to track remote branch 'dev' from 'origin'.
    Switched to a new branch 'dev'
    

    上面这种做法本质是在本地仓库创建了一个跟踪分支,这种操作十分常见,因此 Git 也提供了另一种便捷方法,可以直接让我们检出远程分支到本地同名追踪分支上,如下所示:

    # 可以使用 checkout 命令
    $ git checkout --track origin/dev
    
    # 或者使用最新的 switch 命令
    $ git switch --track origin/dev
    

    上述命令会自动在本地创建一个同名分支dev(前提是本地不存在该分支),并将远程分支origin/dev检出到该分支dev上,然后选项-t, --track会设置本地分支dev追踪远程分支origin/dev

  • 将远程分支合并到本地分支中:可以将远程分支拉取下来,然后手动合并到本地分支中:

    # 拉取远程分支
    $ git fetch origin dev
    
    # 合并远程分支 origin/dev 到本地当前分支
    $ git merge origin/dev
    

查看远程分支

要查看远程分支,只需使用-r, --remotes选项即可:

git branch {-r | --remotes}

删除远程分支

要删除远程分支,大致有如下几种方法:

  • 只删除本地远程分支:如下方法只会删除本地已下载的远程分支,服务器远程仓库的分支不会受到影响:

    git branch -d -r <remote/branch_name>
    

    举个例子:比如,删除本地远程分支origin/dev

    # 本地存在远程分支 origin/develop
    $ git branch -r | grep -i develop
      origin/develop
    
    # 删除本地远程分支 origin/develop
    $ git branch -r -d origin/develop
    Deleted remote-tracking branch origin/develop (was b51743f).
    
    # 本地不存在分支 origin/develop
    $ git branch -r | grep -i develop
    
  • 同时删除本地和服务器远程分支:要同时删除本地和服务器的远程分支,有如下两种方法:

    # 法一
    git push <remote> --delete <branch_name>
    
    # 法二
    git push <remote>  :<branch_name>
    

    举个例子:比如,删除远程仓库origindev分支:

    # 查看本地所有分支
    $ git branch -a | grep -i dev
    * dev                             # 本地分支
      remotes/origin/dev              # 远程分支
    
                                      # 删除远程分支
    $ git push origin dev --delete
    To github.com:Why8n/learn_git.git
     - [deleted]         dev
    
    # 本地和服务器的远程分支均被删除
    $ git branch -a | grep -i dev
    * dev                             # 本地追踪分支仍存在
    

    :删除远程分支不会对本地追踪分支有任何影响,仅仅只是失去追踪目标分支而已,但其内容与历史记录仍存在。

远程标签

对远程标签的操作,主要有如下内容:

推送标签

Git 中推送标签到远程仓库,主要有如下两种操作:

  • 推送具体标签:要推送一个具体标签到服务器,语法如下:

    git push <remote> <tag_name>
    

    举个例子:比如,对当前分支最新版本创建一个标签v1.0,然后将其推送到远程仓库中:

    $ git tag v1.0 HEAD
    
    $ git push origin v1.0
    Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
    To github.com:Why8n/learn_git.git
     * [new tag]         v1.0 -> v1.0
    
  • 推送全部标签:如果要推送所有标签到服务器,命令如下:

    $ git push origin --tags
    

    :如果在推送分支的同时,只要指定了--tags,那么效果是所有分支上的标签都会上传到服务器,而不是只上传当前分支的标签。

下载指定标签

默认情况下,git clone会下载所有的远程标签,但是本地不会进行检出。我们也可以添加-b, --branch直接选择下载指定标签,这样下载完成后会自动检出该标签。

注:git clone -b <tag>下载完成后会自动检出该标签内容到工作目录,但会导致 Git 处于游离态。

举个例子:比如想检出远程标签v1.0,则有如下做法:

# 直接指定要下载的标签。
$ git clone 'git@github.com:Why8n/learn_git.git' -b v1.0 

删除远程标签

要删除服务器上的远程标签,大致有如下几种方法:

# 法一
git push <remote> --delete <tag>

# 法二
git push <remote> :<tag>

:删除远程标签并不会同时删除本地相同标签,如果还需要删除本地标签,可以使用git tag -d <tag>命令。

举个例子:比如现在要删除服务器远程仓库origin的标签v1.0

# 查询远程仓库,此时存在 v1.0
$ git ls-remote --tags origin | grep -i v1.0
b51743fac29d8a99557c560bb517971eb77a2725        refs/tags/v1.0

# 删除远程仓库标签 v1.0
$ git push origin --delete v1.0              # 或者:git push origin :v1.0
To github.com:Why8n/learn_git.git
 - [deleted]         v1.0

# 再次查询远程仓库,v1.0 已被删除
$ git ls-remote --tags origin | grep -i v1.0
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容