《Git权威指南》读书笔记 (Part 1)

本系列为《Git权威指南》的读书笔记,分为两个部分:Part 1 涵盖了书中第 1~3 篇共 20 章的内容,Part 2 为剩余部分。

git format-patch 命令

将从 v1 开始的历次提交逐一导出为补丁文件:

$ git format-patch v1..HEAD
001-Fix-typo-help-to-help.patch
002-Add-I18N-support.patch
003-Translate-for-Chinese.patch

和 git commit 命令一样,git format-patch 命令也可使用 -s--signoff 参数在导出的补丁文件中自动添加 Signed-off-by 行。

git send-email 命令

通过邮件将补丁文件发出:

$ git send-email *.patch

Git 创建的补丁文件使用了 Git 扩展格式,因此在导入时为了避免数据遗漏,要使用 Git 提供的命令而不能使用 GNU 的 patch 命令。即使要导入的不是 Git 版本库,也可以使用 Git 命令。

命令补齐

Linux 的 shell 环境(bash)通过 bash-completion 软件包提供命令补齐功能。如果通过包管理器方式安装 Git,一般都已经为 Git 配置好了自动补齐,但如果是以源码编译的方式安装 Git,就需要为命令补齐多做些工作,具体操作过程如下:

(1)将 Git 源码包中的命令补齐脚本复制到 bash-completion 对应的目录中。

$ cp contrib/completion/git-completion.bash /etc/bash_completion.d/

(2)重新加载自动补齐脚本,使之在当前的 shell 中生效。

$ . /etc/bash_completion

(3)为了能够在终端开启时自动加载 bash_completion 脚本,需要在系统配置文件 /etc/profile 及本地配置文件 ~/.bashrc 中添加下面的内容。

if [ -f /etc/bash_completion ]; then
    . /etc/bash_completion
fi

Linux 平台下的中文支持

先使用 locale 命令查看 Linux 系统使用的字符集。若 Linux 平台使用的是 UTF-8 字符集,在默认设置下,中文文件名在工作区状态输出、查看历史更改概要,以及在补丁文件中,文件名中的中文不能正确地显示,而是显示为八进制的字符编码。通过将 Git 配置变量 core.quotepath 设置为 false,就可以解决中文文件名在这些 Git 命令输出中的显示问题。

$ git config --global core.quotepath false

若 Linux 平台使用的是 GBK 字符集,还需要将显示提交说明所使用的字符集设置为 gbk

$ git config --global i18n.logOutputEncoding gbk

同时设置录入提交说明时所使用的字符集,以便在 commit 对象中正确标注字符集:

$ git config --global i18n.commitEncoding gbk

若是在 Windows 下使用 SecureCRT 软件 ssh 登录到 Linux 平台,则需要将 SecureCRT 的字符编码设置为 UTF-8。设置路径:会话选项 -> 终端 -> 外观 -> 字符编码。

Cygwin 中用户主目录不一致问题

Cygwin 确定用户主目录有几个不同的依据,要按照顺序确定:首先查看系统的 HOME 环境变量,其次查看 /etc/passwd 中为用户设置的主目录。有的软件遵照这个原则,而有些 Cygwin 应用如 SSH,却没有使用 HOME 环境变量而是直接使用 /etc/passwd 中的设置。要避免此问题,有两种方法:

(1)修改 Cygwin 启动的批处理文件(如 C:\cygwin\Cygwin.bat),在开头添加如下一行代码将 Cygwin 的 HOME 环境变量设置为空:

set HOME=

(2)如果希望使用 HOME 环境变量指向的主目录,可编辑 /etc/passwd 文件,将其中的用户主目录修改成 HOME 环境变量所指向的目录。

Cygwin 下命令行补齐忽略文件名大小写

编辑文件 ~/.inputrc,在其中添加设置 set completion-ignore-case on,或者取消已有的相关设置前面的 # 号注释符,修改后需要重新进入 Cygwin。

忽略文件权限的可执行位

虽然 Cygwin 可以模拟 Linux 下的文件授权并对文件的可执行位提供支持,但为支持文件权限而调用 Cygwin 的 stat() 函数和 lstat() 函数会比调用 Windows 自身的 Win32 API 要慢一半(参见 git-config(1)core.ignoreCygwinFSTricks 的介绍,在较新版本的 Git 中已没有这个配置选项)。

若想要 git 不跟踪版本库中文件的权限位,可使用如下命令设置:

$ git config --system core.fileMode false

这样设置以后,版本库中新增的文件,无论文件本身是否设置为可执行,都以 100644 的权限(忽略可执行位)进行添加。

设置 Git 使用的 SSH 客户端

通过设置 GIT_SSH 环境变量即可实现:

$ export GIT_SSH=/cygdrive/c/Program\ Files/PuTTY/plink.exe

首次连接一个使用 SSH 协议的 Git 服务器时,很可能会因为远程 SSH 服务器的公钥没有经过确认而导致 git 命令执行失败,解决办法是直接运行 plink.exe 连接一次远程 SSH 服务器,并对公钥确认进行应答即可。

$ /cygdrive/c/Program\ Files/PuTTY/plink.exe user@ip:port

Cygwin 与 MinGW 的区别

Cygwin 是通过 cygwin1.dll 使用 Windows 的系统调用,而 MinGW 是直接调用 Windows 的系统调用。

msysGit 名字前面的四个字母来源于 MSYS 项目。MSYS 项目源自于 MinGW(Minimalist GNU for Windows,最简 GNU 工具集),通过增加一个 bash 提供的 shell 环境及其他相关的工具软件组成了一个最简系统(Minimal SYStem),简称 MSYS。利用 MinGW 提供的工具和 Git 针对 MinGW 的一个分支版本,在 Windows 平台为 Git 编译出一个原生应用,结合 MSYS 就组成了 msysGit。

msysGit 中 shell 环境的中文支持

1、中文录入问题

为了能在 msysGit 的 shell 界面中输入中文,需要修改 /etc/inputrc,增加或修改相关的配置如下:

# disable/enable 8bit input
set meta-flag on
set input-meta on
set output-meta on
set convert-meta off

修改后需重启 Git Bash 生效。

2、分页器中文输出问题

Git 使用了大量的 less 命令作为分页器,less 命令出现乱码,是因为该命令没有把中文当作正常的字符,可以通过将 LESSCHARSET 环境变量设置为 UTF-8 解决:

$ export LESSCHARSET=utf-8

编辑 /etc/profile,将对环境变量 LESSCHARSET 的设置加入其中,以便于 msysGit 的 shell 环境启动时就加载:

declare -x LESSCHARSET=utf-8

3、ls 命令显示中文文件名

ls 命令在显示中文文件名时显示为一串问号,通过给 ls 命令添加参数 --show-control-chars 解决,为方便起见,可以为 ls 命令设置一个别名:

$ alias ls="ls --show-control-chars"

同样可将上面的 alias 命令添加到 /etc/profile 中实现每次运行 Git Bash 时自动加载。

msysGit 中 Git 的中文支持

和使用 GBK 字符集的 Linux 环境一样,可通过以下三个配置解决中文乱码问题:

$ git config --system i18n.logOutputEncoding gbk
$ git config --system i18n.commitEncoding gbk
$ git config --system core.quotepath false

注意:此处使用的是 --system 级别的配置选项,以使配置仅对此 msysGit 有效而不影响其它的 msysGit 或 Cygwin 等等。

设置 Git 命令别名

$ git config --global alias.st status
$ git config --global alias.ci commit
$ git config --global alias.co checkout
$ git config --global alias.br branch

git init 命令

Git 1.6.5 或更高版本中,在 git init 命令后面直接输入目录名称,自动完成目录的创建,如: git init demo

git rev-parse 命令

假如工作区目录为 /demo,当前目录为 /demo/a/b/c。显示版本库 .git 目录所在的位置:

$ git rev-parse --git-dir
/demo/.git

显示工作区根目录:

$ git rev-parse --show-toplevel
/demo

显示相对于工作区根目录的相对目录:

$ git rev-parse --show-prefix
a/b/c

显示从当前目录后退到工作区的根的深度:

$ git rev-parse --show-cdup
../../../

显示引用对应的提交 ID:

$ git rev-parse master refs/heads/master HEAD
d499e503643abb90090eaf3691e569f5f6803db8
d499e503643abb90090eaf3691e569f5f6803db8
d499e503643abb90090eaf3691e569f5f6803db8

git config 命令的三个级别

--local--global--system 分别为版本库级别、(当前用户)全局级别、系统级别的配置,依次对应 /demo/.git/config 文件、~/.gitconfig 文件和 /etc/gitconfig 文件,三者优先级依次降低,即范围越小的配置文件优先级越高。在 git config 命令中可省略 --local, 默认就是版本库级别的配置。可使用 -e 参数打开相应级别的配置文件进行编辑。

$ git config -e
$ git config -e --global
$ git config -e --system

可以使用 git config 命令操作任何其他的 ini 文件:

$ GIT_CONFIG=test.ini git config a.b.c.d "hello, world"
$ GIT_CONFIG=test.ini git config a.b.c.d

git log 命令

git log 默认只显示作者及其日期,加上 --pretty=fuller 可同时显示提交者及其日期,而 --pretty=full 显示的是作者和提交者,没有日期信息。

使用参数 -p 可在显示日志的同时显示改动。git log -p -1 的输出相当于 git log -1 的输出加上 git diff HEAD~.. 的输出。

当合并或拣选操作发生冲突时,可使用 git log 命令查看到底是哪些提交引起的冲突,例如,在将 hello-1.x^1 拣选到 master 分支上时发生了冲突:

$ git checkout master
$ git cherry-pick hello-1.x^1
$ git log master...hello-1.x^1

git commit --amend 同时重置作者和提交者

git commit --amend 默认只重置提交者(Commit)的信息,使用 --reset-author 参数可同时重置作者(Author)的信息。

$ git commit --amend --allow-empty --reset-author

另外,git commit 命令加上 -s--signoff 参数,可在提交信息中自动加入 Signed-off-by 行。

git status 命令

git status 命令(或者 git diff 命令)在扫描工作区改动时,先判断 .git/index 文件中记录的(用于跟踪工作区文件的)时间戳、长度等信息是否改变,若有改动才继续比较文件内容,同时将新的时间戳和文件长度记录到 .git/index 中,实现工作区状态快速扫描。

  • -s: 输出精简格式的状态,其中位于第一列的字母表示版本库中的文件与暂存区中的文件的差异,第二列的字母表示暂存区中的文件与工作区当前的文件的差异。
  • -b: 当使用了 -s 参数时,同时显示当前工作分支的名称(1.7.2 以后加入的参数)。当 git status 不带任何参数时,默认会显示当前工作分支的名称。
  • --ignored: 同时显示被忽略的文件。

git checkout 命令

  • git checkout .git checkout -- <file> 命令:将暂存区文件替换工作区文件。
  • git checkout HEAD .git checkout HEAD <file> 命令:将 HEAD 指向的版本库文件同时替换暂存区和工作区文件。

git clean 命令

使用 git clean -fd 命令可清除当前工作区中没有加入版本库的文件和目录(未跟踪文件和目录)。

git ls-tree 命令

git ls-tree XXX 列出目录树对象的内容,即对应的目录包含的子目录和文件。XXX 是目录树对象的 ID。

  • -l: 显示其中的文件的大小。
  • -r: 递归显示目录内容。
  • -t: 将递归过程中遇到的每棵树都显示出来,而不只是显示最终的文件。

git ls-files 命令

显示暂存区或工作区中的文件的信息。

git write-tree 命令

将当前暂存区的目录树写入 Git 对象库,输出目录树对象的 SHA1 ID。

git diff 命令

  • git diff: 工作区和暂存区比较。
  • git diff --cached: 暂存区和 HEAD 比较。
  • git diff HEAD: 工作区和 HEAD 比较。
  • git diff --word-diff: 使用逐词比较。默认是逐行比较。

git cat-file 命令

研究 Git 对象 ID 的一个重量级武器就是 git cat-file 命令。

  • -t: 显示对象类型。
  • -p: 显示对象内容。

.git/HEAD 文件

.git/HEAD 文件是一个文本文件,其内容表示当前的 HEAD 指针指向的引用,如:

$ cat .git/HEAD
ref: refs/heads/master

又如:

$ cat .git/HEAD
ref: refs/heads/bugfix/0822

而 .git/refs/heads/master 和 .git/refs/heads/bugfix/0822 也是文本文件,其内容为对应分支的最新 commit ID。

SHA1 哈希值的生成方法

生成 SHA1 哈希值的规则:对象类型 对象字符数 <null> 对象具体内容,参见 git hash-object 命令一节。

(1) commit

$ git cat-file commit HEAD | wc -c
256
$ ( printf "commit 256\000"; git cat-file commit HEAD ) | sha1sum
d499e503643abb90090eaf3691e569f5f6803db8 -
$ git rev-parse HEAD
d499e503643abb90090eaf3691e569f5f6803db8

(2) blob

$ git cat-file blob HEAD:welcome.txt | wc -c
25
$ ( printf "blob 25\000"; git cat-file blob HEAD:welcome.txt ) | sha1sum
fd3c069c1de4f4bc9b15940f490aeb48852f3c42 -
$ git rev-parse HEAD:welcome.txt
fd3c069c1de4f4bc9b15940f490aeb48852f3c42

(3) tree

$ git cat-file tree HEAD^{tree} | wc -c
39
$ ( printf "tree 39\000"; git cat-file tree HEAD^{tree} ) | sha1sum
f58da9a820e3fd9d84ab2ca2f1b467ac265038f9 -
$ git rev-parse HEAD^{tree}
f58da9a820e3fd9d84ab2ca2f1b467ac265038f9

(4) tag

$ git cat-file tag refs/tags/V3.40.00.00B63 | wc -c
181
$ ( printf "tag 181\000"; git cat-file tag refs/tags/V3.40.00.00B63 ) | sha1sum
d4b699834e6254aca9398f1036848054f2dbd3e1 -
$ git rev-parse refs/tags/V3.40.00.00B63
d4b699834e6254aca9398f1036848054f2dbd3e1

Git 库对象的表示方法

  • 采用部分的 SHA1 哈希值,不必把 40 位写全,只要不与现有的其他哈希值冲突即可(满足前缀码要求),通常只写 7 位。
  • 使用 master 代表分支 master 的最新提交,也可使用全称 refs/heads/masterheads/master
  • 使用 HEAD 代表版本库中最近的一次提交。
  • 符号 ^ 用于指代父提交。如 HEAD^ 代表版本库中的上一次提交,HEAD^^ 代表 HEAD^ 的父提交。
  • 里程碑所指向的提交或者提交本身,可用 A^{}A^0A~0A^{commit} 表示。
  • 对于一个提交有多个父提交,可在符号 ^ 后面用数字表示是第几个父提交,如 a573106^2, HEAD^1, HEAD^^2 等等。
  • 符号 ~<n> 也可以用于指代祖先提交,如 a573106~5 相当于 a573106^^^^^。假如祖先提交有多个分支,如何表示?
  • 提交所对应的树对象,使用类似 a573106^{tree}a573106: 访问。
  • 某次提交对应的文件对象,使用类似 a573106:path/to/file 访问,即某次提交对应的树下的文件对象。
  • 暂存区中的文件对象,使用类似 :path/to/file 访问。
  • reflog表示法:<refname>@{<n>} 表示引用 <refname> 之前第 <n> 次改变时的 SHA1 哈希值。<refname> 也包括 stash。

git reflog 命令

.git/logs 目录下日志文件记录了分支的变更,其中包括 HEAD 和 stash,例如:

$ tree .git/logs
.git/logs
├── HEAD
└── refs
    ├── heads
    │   └── master
    └── stash

2 directories, 3 files

git reflog 命令用于对这些文件进行操作,使用 show 子命令可以显示文件内容,例如:

$ git reflog show master
88556bf master@{0}: 88556bf: updating HEAD
d499e50 master@{1}: HEAD^: updating HEAD
1f98a07 master@{2}: commit: does master follow this new commit?
d499e50 master@{3}: commit: which version checked in?
21b22f7 master@{4}: commit: who does commit?

使用 expire 子命令对 reflog 做过期操作,可同时提供 --expire=<date> 参数,例如:

$ git reflog expire --expire=now --all

默认创建的带工作区的版本库都会包含 core.logallrefupdates 为 true 的配置,这样在版本库中建立的每个分支都会创建对应的 reflog。但是创建的裸版本库默认不包含这个设置,也就不会为每个分支设置 reflog。如果团队的规模较小,可能因为分支误操作导致数据丢失,可以考虑为裸版本库添加 core.logallrefupdates 的相关配置。

git reset 命令

  • --soft: 仅更改 master 等引用的指向。
  • --mixed: 同时更改引用的指向和暂存区(不使用参数时默认为 --mixed),相当于对 git add 的反向操作。
  • --hard: 同时更改引用的指向、暂存区和工作区。

git commit --amend 修补提交命令实际上相当于执行了以下两条命令(.git/COMMIT_EDITMSG 保存了上次的提交日志):

$ git reset ---soft HEAD^
$ git commit -e -F .git/COMMIT_EDITMSG

git checkout 命令

「分离头指针(detached HEAD)」状态指的是 HEAD 头指针指向了一个具体的提交 ID,而不是一个引用(分支)。实际上除了以 refs/heads 为前缀的引用之外,如果检出任何其他的引用,都将使用工作区处于分离头指针状态。

与 git reset 命令的区别:git reset 命令直接变更的是 master 等「游标」,但 HEAD 本身一直指向 refs/heads/master,从而被间接变更;而 git checkout 命令影响的是 HEAD 本身的指向,master 等「游标」不受影响。不带参数时,git reset 默认值是 HEAD(将 master 等游标指向 HEAD,相当于引用没有重置,但默认选项是 –mixed,所以暂存区会被重置为 HEAD 指向的目录树),而 git checkout 默认值是暂存区(使用暂存区的目录树覆盖工作区的目录树)。因此,git reset 命令一般用于重置暂存区(当使用 --hard 参数时才重置工作区),而 git checkout 命令主要是覆盖工作区(若指定了 git checkout <commit>,则也会替换暂存区中相应的文件)。

  • git checkout branch: 检出 branch 分支。更新 HEAD 以指向 branch 分支,以及用 branch 指向的树更新暂存区和工作区。
  • git checkout [HEAD]: 汇总显示工作区、暂存区与 HEAD 的差异(状态检查)。
  • git checkout -- filename: 用暂存区 filename 覆盖工作区 filename。相当于取消自上次执行 git add filename 以来的本地修改。
  • git checkout branch -- filename: 维持 HEAD 的指向不变,用 branch 指向的提交中的 filename 替换暂存区和工作区中相应的文件。
  • git checkout -- .git checkout . : 用暂存区的「所有文件」「直接覆盖」工作区文件。

git stash 命令

  • git stash pop--index 选项时,除了恢复工作区的文件外,还尝试恢复暂存区。
  • git stash save "message...": 保存工作进度时添加说明。加 -k 或 --keep-index 参数,在保存进度后不会将暂存区重置。默认会将暂存区和工作区强制重置。
  • git stash apply [--index] [<stash>]: 除了不删除恢复的进度外,与 git stash pop 相同。
  • git stash drop [<stash>]: 删除一个保存的进度。默认删除最新进度。
  • git stash clear: 删除所有保存的进度。
  • git stash branch <branchname> <stash>: 基于进度创建分支。

git stash 就是使用引用和引用变更日志(reflog)来实现的,且使用两个提交分别对应暂存区进度和工作区进度,可使用 git log refs/stash 等命令查看。

  • git reflog show refs/stash: 查看 git stash 的引用日志。
  • git log refs/stash 或 stash@{0} 或 stash@{1}: 查看对应的进度的提交历史。

git --exec-path

查看 git 安装路径。

.gitignore 语法

  • 忽略文件中的空行或以井号(#)开始的行会被忽略。
  • 可以使用通配符,参见 glob(7)。例如:星号(*)代表任意多个字符,问号(?)代表任意一个字符,方括号([abc])代表可选字符范围等。
  • 如果名称的最面是一个路径分隔符(/),表明要忽略的文件在此目录下,而非子目录的文件。
  • 如果名称的最面是一个路径分隔符(/),表明要忽略的是整个目录,同名文件不忽略,否则同名的文件和目录都忽略。
  • 通过在名称的最前面添加一个感叹号(!),代表不忽略。
  • 忽略只对未跟踪文件有效,对于已加入版本库的文件无效。

例如:

  • *.a # 忽略所有以 .a 为扩展名的文件。
  • !lib.a # 但是 lib.a 文件或目录不忽略,即使前面设置了对 *.a 的忽略。
  • /TODO # 只忽略此目录下的 TODO 文件,子目录的 TODO 文件不忽略。
  • build/ # 忽略所有 build/ 目录下的文件。
  • doc/*.txt # 忽略文件如 doc/notes.txt,但是文件如 doc/server/arch.txt 不被忽略。

git archive 命令

用于文件归档,例如:

  • git archive -o latest.zip HEAD: 基于最新提交建立归档文件 latest.zip。
  • git archive -o partial.tar HEAD src doc: 只将目录 src 和 doc 建立到归档 partial.tar 中。
  • git archive --format=tar --prefix=1.0/ v1.0 | gzip > foo-1.0.tar.gz: 基于里程碑 v1.0 建立归档,并且为归档中的文件添加目录前缀 1.0。
  • git get-tar-commit-id < partial.tar: 获取归档包中的提交 ID。注意,需要是使用 tar 格式建立的归档,且使用提交 ID 或里程碑 ID 创建,才会将提交 ID 记录在归档文件的头部。使用树 ID 创建的归档文件不会记录提交 ID。

git rev-parse 命令

  • git rev-parse --symbolic --branches: 显示分支
  • git rev-parse --symbolic --tags: 显示里程碑
  • git rev-parse --symbolic --glob=refs/*: 显示所有引用

里程碑实际指向的是一个 Tag 对象而非提交,轻量级里程碑是直接指向提交的里程碑。对于里程碑 A,不管是否轻量级里程碑,甚至是提交本身,可用下面的三个表示法显示提交本身的哈希值而非里程碑对象的哈希值:

// 作用于里程碑
$ git rev-parse A^{} A^0 A^{commit}
82b77ede0e3fc80f0c4b59cb10bfcd33a0b8168a
82b77ede0e3fc80f0c4b59cb10bfcd33a0b8168a
82b77ede0e3fc80f0c4b59cb10bfcd33a0b8168a

// 作用于提交本身
$ git rev-parse master^{} master^0 master^{commit}
8a2b854fec98309e539c8961f6ca69600fd9a343
8a2b854fec98309e539c8961f6ca69600fd9a343
8a2b854fec98309e539c8961f6ca69600fd9a343

在提交日志中查找字符串的方式显示提交,仅显示搜索到的最新的提交,支持正则表达式:

$ git rev-parse :/Commit A

git rev-list 命令

  • 一个提交 ID 代表从该版本开始的所有历史提交,如 git rev-list --oneline A
  • 两个或多个版本,相当于每个版本单独使用时指代的列表的并集,如 git rev-list --oneline D F
  • 取反:排除这个版本及其历史版本,如 git rev-list ---oneline ^G D。两个参数顺序不重要,^G D 与 D ^G 等价。
  • 两点表示法:如与上面等价的 git rev-list --oneline G..D。两个版本的前后顺序很重要,G..D 相当于 ^G D,而 D..G 相当于 ^D G。
  • 三点表示法:两个版本共同能够访问到的除外,相当于差集。两个版本的前后顺序没有关系,B…C 与 C…B 等价。
  • 某提交的历史提交,自身除外,用语法 r1^@ 表示。
  • 提交本身不包括其历史提交,用语法 r1^! 表示。

git describe 命令

使用 git describe 命令将提交显示为一个易记的名称。这个易记的名称来自于建立在该提交上的里程碑,若该提交没有里程碑则使用该提交历史版本上的里程碑并加上可理解的寻址信息。

  • 如果该提交恰好被打上一个里程碑,则显示该里程碑的名字。
  • 若提交没有对应的里程碑,但是在其祖先版本上建有里程碑,则使用类似 <tag>-<num>-g<commit> 的格式显示,其中 <tag> 是最接近的祖先提交的里程碑名字,<num> 是该里程碑和提交之间的距离,<commit> 是该提交的精简提交 ID,例如:old_practice-3-g8a2b854
  • 如果工作区对文件有修改,还可以通过 --dirty 选项加上 -dirty 后缀表示出来,例如: jx/v1.0-dirty
  • 默认不使用轻量级里程碑生成版本描述字符串,需要使用 --tags 参数才能使用所有里程碑(包含轻量级里程碑)。
  • 如果提交本身没有包含里程碑,可以通过传递 --always 参数显示精简提交 ID,否则会出现,例如:
$ git describe master^ --always
75346b3

git name-rev 命令

git name-rev 命令和 git describe 类似,会显示提交 ID 及其对应的一个引用。默认优先使用分支名,除非使用 –tags 参数。还有一个显著的不同就是,如果提交上没有相对应的引用,则会使用最新提交上的引用名称并加上向后回溯的符号 ~<num>。当对里程碑本身调用 git name-rev 命令时,会在对应的里程碑引用名称后面加上后缀 ^0,是因为该引用指向的是一个 tag 对象而非提交,用 ^0 后缀指向对应的提交,如:

$ git name-rev HEAD --tags
HEAD tags/jx/v1.0^0

还可以对标准输入中的提交 ID 进行改写,使用管道符号对前一个命令的输出进行改写,例如:

$ git log --pretty=oneline origin/helper/master | git name-rev --tags --stdin
bb4fef88fee435bfac04b8389cf193d9c04105a6 **(tags/jx/v2.3^0)** Translate for Chinese.
610e78fc95bf2324dc5595fa684e08e1089f5757 **(tags/jx/v2.3~1)** Add I18N support.
384f1e0d5106c9c6033311a608b91c69332fe0a8 **(tags/jx/v2.2^0)** Bugfix: allow spaces in username.
e5e62107f8f8d0a5358c3aff993cf874935bb7fb **(tags/jx/v2.1^0)** fixed typo: -help to --help
5d7657b2f1a8e595c01c812dd5b2f67ea133f456 **(tags/jx/v2.0^0)** Parse arguments using getopt_long.
3e6070eb2062746861b20e1e6235fed6f6d15609 **(tags/jx/v1.0^0)** Show version.
75346b3283da5d8117f3fe66815f8aaaf5387321 **(tags/jx/v1.0~1)** Hello world initialized.

git blame 命令

当针对文件执行 git blame 命令时,就会逐行显示文件,在每一行的行首显示此行最近是在什么版本引入的,由谁引入的。只想查看某几行,使用 -L <start>,<end> 参数,<start><end> 可以使用绝对数值,<end> 还可以使用 +offset 或 -offset 表示相对 <start> 行向下或向上的行数。例如:

// 显示 README 文件的第 6~8 行
$ git blame -L 6,8 README
// 显示 README 文件第 6 行开始的 8 行(即第 6~13 行)
$ git blame -L 6,+8 README

git bisect 二分查找命令

  • git bisect start: 开始二分查找。
  • git bisect bad: 将当前版本(HEAD)标记为「坏提交」。
  • git bisect good G: 将 G 版本标记为「好提交」。
  • git bisect good: 将自动定位到的版本标记为好提交。
  • git bisect bad: 将自动定位到的版本标记为坏提交。
  • 重复上述两步,直到输出显示已经成功定位到引入坏提交的最接近的版本。
  • git checkout bisect/bad: 切换到出问题的版本。
  • git bisect reset: 结束二分查找。

以上前三步可以合并为一步: git bisect start HEAD G。假如某一步将版本标记错误,使用如下步骤恢复并继续:

  • git bisect log > logfile: 将二分查找的日志保存在一个文件中。
  • vim logfile: 编辑日志文件,将记录了错误动作的行删除。
  • git bisect reset: 结束正在进行的出错的二分查找。
  • git bisect replay logfile: 通过日志文件恢复进度,重启二分查找。
  • git bisect good: 重新标记并继续。

在 start 之后,还可使用 run 子命令运行自动化测试脚本实现自动查找:

$ git bisect start master G
$ git bisect run sh good-or-bad.sh
$ git describe refs/bisect/bad

自动化测试脚本的书写规则:

  • 如果脚本的退出码是 0,则正在测试的版本是一个「好版本」。
  • 如果脚本的退出码是 125,则正在测试的版本被跳过。
  • 如果脚本的退出码是 1 到 127(125 除外),则正在测试的版本是一个「坏版本」。

合并最近的两个提交

例如需要将最近的两个试验性的提交合并为一个完整的提交。先使用 --soft 参数调用重置命令,将引用回退到最近两次提交之前,这样暂存区和工作区都有最近的两个提交的修改,实现了合并:

$ git reset --soft HEAD^^

再执行提交操作,即完成最新两个提交合并为一个提交的操作:

$ git commit -m "Squash two commits into one."

git cherry-pick 命令

挑选命令 git cherry-pick 的含义是从众多的提交中挑选出一个提交应用在当前的工作分支中。该命令需要提供一个提交 ID 作为参数,操作过程相当于将该提交导出为补丁文件,然后在当前 HEAD 上重放,形成无论内容还是提交说明都一致的提交。例如:

$ git checkout C
$ git cherry-pick master^
$ git cherry-pick master

最后需要将 master 分支重置到新的提交 ID 上:

$ git checkout master
$ git reset --hard HEAD@{1}

即先切回 master 分支,使得之前的分离头指针状态下指向的提交 ID 记录在 HEAD@{1} 中,然后使用 git reset --hard HEAD@{1} 命令将头指针、暂存区、工作区都重置到新的提交 ID 上。

合并历史中连续的两个提交

假如要将历史中连续的两个提交 C 和 D 合并为一个:

// 先切换到要合并的两个提交中较新的一个,此处为 D
$ git checkout D
// 仅将引用重置到前两次的提交,这样暂存区和工作区都有 C 和 D 两次提交的改动
$ git reset --soft HEAD^^
// `-C <commit>` 参数表示重用 `<commit>` 的提交说明
$ git commit -C C
// 将 D 之后的所有提交都挑选到新的提交上
$ git cherry-pick E
$ git cherry-pick F
// 将 master 分支指向新的提交
$ git checkout master
$ git reset --hard HEAD@{1}

git rebase 命令

变基命令的完整格式:

$ git rebase --onto <newbase> <since> <till>

其中,<since> 不能省略;若省略 <newbase>,则其默认值就是 <since>;若省略 <till>,则其默认值为 HEAD。当 <till> 参数为 master 时,变基操作会直接修改 master 分支,而无需再对 master 进行重置操作。

变基操作的过程:

  • 首先会执行 git checkout 切换到 <till>
  • <since>..<till> 所标识的提交范围写到一个临时文件中。
  • 将当前分支强制重置到 <newbase>,相当于执行: git reset --hard <newbase>
  • 从保存在临时文件中的提交列表中,将提交逐一按顺序重新提交到重置之后的分支上。
  • 如果遇到提交已经在分支中包含,则跳过该提交。
  • 如果在提交过程遇到冲突,则变基过程暂停。用户解决冲突后,执行 git rebase --continue 继续变基操作,或者执行 git rebase --skip 跳过此提交,或者执行 git rebase --abort 就此终止变基操作并切换到变基前的分支上。

例如,通过变基操作将特性分支 user2/i18n 合并到主线,操作如下:

$ git checkout user2/i18n
$ git rebase master

此例中,<since><newbase> 都为 master,而 <till> 为 user2/i18n,因此在进行变基操作时会先切换到 user2/i18n 分支,并强制重置到 master 分支所指向的提交,然后再将原 user2/i18n 分支的提交一一拣选到 master 分支上,成为新的 user2/i18n 分支,而 master 分支不变。

交互式变基操作使用 -i 参数,会将 <since>..<till> 的提交悉数罗列在一个文件中,然后自动打开一个编辑器来编辑这个文件,通过修改文件的内容设定变基的操作,实现删除提交、将多个提交压缩为一个提交、更改提交的顺序,以及更改历史提交的提交说明等。文件中的动作包括:

  • p, pick: 应用此提交。
  • r, reword: 应用此提交,但允许用户修改提交说明。
  • e, edit: 应用此提交,但在应用后暂停,提示用户使用 git commit --amend 执行提交,以便对提交进行修补。此后还需执行 git rebase --continue 继续变基操作。在变基暂停状态下可以执行多次提交,从而实现把一个提交分解为多个提交的目的。
  • s, squash: 该提交会与前面的提交压缩为一个。
  • f, fixup: 与 squash 类似,但此提交的提交说明会被丢弃。
  • x <cmd>, exec <cmd>: 执行 shell 命令 <cmd>,若失败则停止。

git commit-tree 命令

使用此命令可基于特定的树对象创建新的提交,并将新的提交 ID 打印输出。-p 参数指定父提交,可同时使用多个 -p 参数指定多个父提交,若不加 -p 参数则创建的是无父提交的根提交。如:

// 先打印出里程碑 A 指向的目录树
$ git cat-file -p A^{tree}
// 从该目录树创建根提交(不指定 -p 参数)
$ echo "Commit from tree of tag A." | git commit-tree A^{tree}
d2e0c2d76abaeb7ad4704af9b2563ba655b418b2
// 将 master 分支里程碑 A 之后的提交全部迁移到根提交 d2e0c2d 上
$ git rebase --onto d2e0c2d A master
// 查看当前已精简的 master 分支历史
$ git log --oneline --graph
* 2bba74c README is from welcome.txt
* d2e0c2d Initial commit.

git hash-object 命令

git hash-object 命令是对「SHA1 哈希值的生成方法」中各个步骤的封装。例如,直接调用 sha1sum 命令生成哈希值的方法如下:

$ git cat-file commit f460ecf | wc -c
256
$ (printf "commit 256\000"; git cat-file commit f460ecf) | sha1sum
f460ecf5ea96e9978716b2a036a7f34c9ad8d077 -

而调用 git hash-object 命令生成哈希值的方法如下:

$ git cat-file commit f460ecf > tmpfile
$ git hash-object -t commit -- tmpfile
f460ecf5ea96e9978716b2a036a7f34c9ad8d077

可以看到,两种方法生成的哈希值相同。

git hash-object 命令增加 -w 参数将会把计算出的哈希值写入对象库中,可用于生成无父提交的根提交。例如:

// 去掉 parent 开头的行
$ git cat-file commit f460ecf | sed -e '/^parent/ d' > tmpfile
// 计算哈希值并写入对象库(增加 -w 选项)
$ git hash-object -t commit -w -- tmpfile
98267d83d7a01e7d781442cb9b8b034fff270c20
// 将 master 分支 f460ecf 之后的提交全部迁移到根提交 98267d8 上
$ git rebase --onto 98267d8 f460ecf master
// 查看当前已精简的 master 分支历史
$ git log --oneline --graph
* 6468929 README is from welcome.txt
* 98267d8 restore file: welcome.txt

git hash-object 命令也可通过 –stdin 选项从标准输入中读取内容,上述前两个步骤可不通过临时文件,而直接写为:

$ git cat-file commit f460ecf | sed -e '/^parent/ d' | git hash-object -t commit -w --stdin

git revert 命令

git revert 命令用于反向提交,即重新创建一个新的提交,此提交的改动是指定提交的反向修改。SVN 中的 revert 命令表示丢弃本地文件的修改,相当于 git checkout HEAD 命令。

git clone 命令

一般约定俗成裸版本库的目录名以 .git 为后缀。--mirror--bare 的不同之处在于 --mirror 的裸版本对上游版本库进行了注册,这样可以在裸版本库中使用 git fetch 命令和上游版本库进行持续同步。

git remote 命令

使用 git remote -v 命令可查看对上游版本库的注册信息,例如:

$ git remote -v
origin ssh://user@ip:port/repo/project (fetch)
origin ssh://user@ip:port/repo/project (push)

git ls-remote 命令

使用 git ls-remote 命令可查看远程版本库的所有引用,例如:

$ git ls-remote
From ssh://user@ip:port/repo/project
5cd47fdec9a64d37535e98fe4889f00560ee1ff1        HEAD
6468f4523fd46803cf720322ffca4ccd400149a4        refs/changes/52/16352/1
5ebb5da2c1fff2456ab4b2bcdb3685c39cda3ffd        refs/changes/53/16353/1
95bca39facb6b773b5822dc40d6b829e353a7773        refs/heads/dev
5cd47fdec9a64d37535e98fe4889f00560ee1ff1        refs/heads/master
d5bdac04ff400aa445ecdb4a8e1ed9f14e891fbf        refs/meta/config

也可使用 -t--tags 参数只查看远程版本库的里程碑,例如:

$ git ls-remote --tags
From ssh://user@ip:port/repo/project
4cee876bf8adafab16292238a9edb99f42f3519a        refs/tags/V3.40.00.00B62GIT
2faf0f3208e1fff1634050d7b57aac662893508c        refs/tags/V3.40.00.00B62GIT^{}
d4b699834e6254aca9398f1036848054f2dbd3e1        refs/tags/V3.40.00.00B63
23227d077679c7897338f39f34845ac31ce98b8b        refs/tags/V3.40.00.00B63^{}
4a72c18225878794f1933ca9f7dd7b1c7166cfda        refs/tags/V3.40.00.00B64
e433492ae3ae5c5ca24fcd9e80acfe8a16545e36        refs/tags/V3.40.00.00B64^{}

使用 -h--heads 参数只查看远程版本库的分支,例如:

$ git ls-remote --heads
From ssh://user@ip:port/repo/project
a567de8e0c32eddfb7852c49c745ae85e0df35d5        refs/heads/master
e433492ae3ae5c5ca24fcd9e80acfe8a16545e36        refs/heads/bugfix/V3.40.00.00B64
a2723a72d9d7180dd3a8019c524ea33373b79e72        refs/heads/bugfix/V3.40.00.00B65
a556b7b94b9f7c7ad82640c39ca012a3a9242bde        refs/heads/bugfix/V3.40.00.00B66
4c690dacdfa9e92770243f01eb693d9ac29badb3        refs/heads/bugfix/V3.40.00.00B67
f19db005b7ca3cf3b1a2ff5eb108480928c0d789        refs/heads/bugfix/V3.40.00.00B68

git show-ref 命令

git show-ref 命令用于查看本地版本库所包含的引用,例如:

$ git show-ref
646892983eb3e56bc26405bc871b7c60a45de05f refs/heads/master
f60120befebcd489651cdf91f8bb7c0de1e362fd refs/tags/old_practice

git pack-refs 命令

Git 对于以 SHA1 哈希值作为目录名和文件名保存的对象有一个术语,称为松散对象。松散对象打包后会提高访问效率,而且不同的对象可以通过增量存储节省磁盘空间。git pack-refs 就是用于打包的命令,打包的引用存放在 .git/packed-refs 中,打包的对象存放在 .git/objects/pack/pack-*.pack 文件中,同名的 .idx 文件为索引文件。

使用 git show-index 命令可查看索引中包含的对象:

$ git show-index < .git/objects/pack/pack-*.idx | head -5
921 092d11fc81e9f0504291ebb8866da479c855fb2c (02994814)
2167 0933f3f3cc3965d59ea7516ac0c1427b1f3ec64f (268fd241)
2836 18832d35117ef2f013c4009f5b2128dfaeff354f (705978ba)
2252 190d840dd3d8fa319bdec6b8112b0957be7ee769 (274ba58f)
1408 1f98a0774dad0a78b3be436d5bad9b8162537f76 (1e0a1125)

git fsck 命令

查看版本库中包含的未被任何引用关联的松散对象,其中标识为 dangling 的对象就是没有被任何引用直接或间接关联到的对象:

$ git fsck
Checking object directories: 100% (256/256), done.
Checking objects: 100% (33/33), done.
dangling commit 40948dd39d6fe5f275dc1f8923bc2375ced692f0
dangling commit d7beec3e9098c550b5aa626fc63b36064cea65c3
dangling commit 6a2ca77690171e5719b0158653b8fec83c4abf06
$ git prune
$ git fsck
Checking object directories: 100% (256/256), done.
Checking objects: 100% (33/33), done.

加上 --no-reflogs 参数后可查看仅被 reflog 引用的对象,这里显示的 dangling commit 需要先做过期操作才能清除:

$ git fsck --no-reflogs
Checking object directories: 100% (256/256), done.
Checking objects: 100% (33/33), done.
dangling commit 8a2b854fec98309e539c8961f6ca69600fd9a343
dangling commit 092d11fc81e9f0504291ebb8866da479c855fb2c
dangling commit 2bba74cdca418a4b2261de46c733c6fb4476ba60
dangling commit bd757aad3264b75f6ce1354f38ab925268185e84
dangling commit de6c604fa086ab4977e7d7b6363f4062a4a8e2e5
dangling commit 70a07f7a7b01a7fc2802ea13233cfd58ff9e7f08
$ git reflog expire ---expire=now --all
$ git prune
$ git fsck --no-reflogs
Checking object directories: 100% (256/256), done.
Checking objects: 100% (33/33), done.

git prune 命令和 git gc 命令

清理版本库实际操作中很少用到 git prune 命令,而是使用更为常用的 git gc 命令,其中包括了打包、丢弃 reflog、清除未被引用对象等等操作,且 git merge 等命令会自动调用 git gc --auto 命令,在版本库确实需要整理时自动开始整理操作。主要的触发条件是:松散对象只有超过一定数量时才会执行。在统计松散对象数量时,为了降低在 .git/objects/ 目录下搜索松散对象对系统造成的负担,实际采取了取样搜索,即只会对对象库下的一个子目录 .git/objects/17 进行文件搜索,在默认的配置下,只有该目录中对象数目超过 27 个才会触发版本库的整理。值 27 由配置变量 gc.auto 控制,其默认值为 6700. 对象目录为 SHA1 值的前两个字符,即 0x00~0xFF,共 256 种可能,gc.auto 为全部松散对象数的阈值。因此,对于单个取样目录,其阈值为 (6700+255)/256=27 (向上取整)。

快进式推送

所谓快进式推送,就是要推送的本地版本库的提交是建立在远程版本库相应分支的现有提交基础上的,即远程版本库相应分支的最新提交是本地版本库最新提交的祖先提交。git push 推送时遇到非快进式推送的错误提示,需先执行 git pull 拉回远程版本库的最新提交与本地合并后再尝试 git push 推送操作。

将版本库的配置变量 receive.denyNonFastForwards 设置为 true 可禁止任何人进行非快进式推送,即使使用 git push -f 强制推送也会被禁止。更好的方法是搭建基于 SSH 协议的 Git 服务器,通过钩子脚本更灵活地进行配置,例如:允许来自某些用户的强制提交,而其他用户不能执行非快进式推送。

git merge 命令

git pull 操作由两个步骤组成:先执行 git fetch,再执行 git merge。合并操作的命令行格式如下:

git merge [选项...] <commit>...

合并操作是将 <commit> 对应的目录树和当前工作分支的目录树的内容进行合并,合并后的提交以当前分支的提交作为第一个父提交,以 <commit> 为第二个父提交。合并操作还支持将多个 <commit> 代表的分支和当前分支进行合并,过程类似。默认情况下,合并后的结果会自动提交,但是如果提供 --no-commit 选项,则合并后的结果会放入暂存区,用户可以对合并结果进行检查、更改,然后手动提交。

合并遇到冲突后,通过以下几个文件记录:

  • 文件 .git/MERGE_HEAD 记录所合并的提交 ID。
  • 文件 .git/MERGE_MSG 记录合并失败的信息。
  • 文件 .git/MERGE_MODE 标识合并状态。

使用 git ls-files 命令查看的第三个字段为暂存区编号,当合并冲突发生后,会看到 0 以上的暂存区编号。

  • 编号为 1 的暂存区用于保存冲突文件修改之前的副本,即冲突双方共同的祖先版本,可以用 :1:<filename> 访问。
  • 编号为 2 的暂存区用于保存当前冲突文件在当前分支中修改的副本,可以用 :2:<filename> 访问。
  • 编号为 3 的暂存区用于保存当前冲突文件在合并版本(分支)中修改的副本,可以用 :3:<filename> 访问。

工作区中的版本使用特殊标记标识,<<<<<<<(七个小于号)和 =======(七个等号)之间的内容是当前分支所更改的内容,=======(七个等号)和 >>>>>>>(七个大于号)之间的内容是所合并的版本更改的内容。当 merge.conflictstyle 配置为 diff3 时(默认为 merge),<<<<<<<||||||| 之间的内容为本地更改版本,|||||||======= 之间的内容为原始(共同祖先)版本,=======>>>>>>> 之间的内容为他人更改的版本。

冲突解决的实质就是通过编辑操作,将冲突标识符所标识的冲突内容替换为合适的内容,并去掉冲突标识符。编辑完成后执行 git add 命令将文件添加到暂存区(编号 0),然后再提交就完成了冲突解决。当然也可以选择使用 git reset 命令放弃合并。

启动图形工具进行冲突解决只需执行命令 git mergetool 即可。修改完成后保存退出即完成图形化冲突解决,此时工作区还会遗留一个以 .orig 结尾的合并前的文件副本。

因为文件名修改造成的冲突,称为树冲突。解决树冲突的方法,可手工将需要保留的文件使用 git add 命令添加,不需要保留的文件使用 git rm 命令删除再提交即可;或者通过 git mergetool 命令(忽略其中的提示和警告)交互式输入 d 将文件删除,或输入 c 将文件保留(创建)再提交。

可给 git merge 命令使用 -s 参数设定合并策略,-X 参数为所选的合并策略提供附加的参数。合并策略包括以下几种:

  • resolve: 只能用于合并两个头(即当前分支和另外一个分支),使用三向合并策略,被认为是最安全、最快的合并策略。
  • recursive: 只能用于合并两个头(即当前分支和另外一个分支),使用三向合并策略,是合并两个头指针时的默认合并策略。有三个选项: (1) ours: 遇到冲突时,使用我们的版本(当前分支的版本),不冲突时会将他人的改动合并进来,即我们的版本优先。注意与下面的 ours 合并策略区分。 (2) theirs: 与 ours 选项相反,遇到冲突时选择他人的版本,丢弃我们的版本。 (3) subtree: 比下面的 subtree 合并策略的定制能力更强。下面的 subtree 合并策略要对两个树的目录移动进行猜测,而 recursive 合并策略可以通过此参数直接对子树目录进行设置。
  • octopus: 可合并两个以上的头指针,但拒绝执行需要手动解决的复杂合并。这是合并三个及三个以上的头指针进行合并时的默认合并策略。
  • ours: 可合并任意数量的头指针,但合并的结果总是使用当前分支的内容,丢弃其他分支的内容。
  • subtree: 这是一个经过调整的 recursive 策略。当合并树 A 和 B 时,如果 B 和 A 的一个子树相同,B 首先进行调整以匹配 A 的树的结构,以免两棵树在同一级别进行合并。同时也针对两棵树的共同祖先进行调整。

git tag 命令

显示里程碑:

  • 不带任何参数执行 git tag 命令即可显示当前版本库的里程碑列表。
  • 使用 -n<num> 参数指定显示最多 <num> 行里程碑的说明。
  • 使用 -l 参数指定通配符对输出进行过滤,只显示名称和通配符相符的里程碑,例如: git tag -l jx/v2*

创建里程碑的命令用法如下:

$ git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] <tagname> [<commit> | <object>]

其中,不带任何参数创建的是轻量级里程碑,加上 -a 参数创建的是带说明的里程碑,加上 -s 参数创建的是带签名的里程碑,或者直接使用 -u <key-id> 指定签名时使用的私钥。

使用 -m 或 -F 参数指定里程碑的说明,若不指定 -a, -s, -u 参数,则隐含了 -a 参数。

如果没有指定 <commit><object> 则是基于头指针 HEAD 创建里程碑。

创建轻量级里程碑后,会在版本库的 .git/refs/tags 目录下创建与里程碑同名的文件,其内容就是对应的提交的 40 位 SHA1 哈希值,例如:

$ cat .git/refs/tags/mytag
60a2f4f31e5dddd777c6ad37388fe6e5520734cb

创建带说明的里程碑和带签名的里程碑后,同样会在 .git/refs/tags 目录下创建与里程碑同名的文件,但其内容是指向 tag 对象的哈希值。

使用 gpg --list-keys 命令查看当前可用的 GnuPG 公钥,例如:

$ gpg --list-keys
/home/jiangxin/.gnupg/pubring.gpg
---------------------------------
pub   1024D/FBC49D01 2006-12-21 [有效至:2016-12-18]
uid                  Jiang Xin <worldhello.net@gmail.com>
uid                  Jiang Xin <jiangxin@ossxp.com>
sub   2048g/448713EB 2006-12-21 [有效至:2016-12-18]

pub   2048R/37379C67 2011-01-02
uid                  User1 <user1@sun.ossxp.com>
sub   2048R/2FCFB3E2 2011-01-02

其中的 FBC49D01 或 37379C67 就可作为 -u 参数的 <key-id> 使用。创建公钥/私钥对使用 gpg --gen-key 命令实现。使用命令 git tag -v <tagname> 验证里程碑签名的有效性。

删除里程碑使用使用 git tag -d <tagname>...。Git 没有提交对里程碑重命名的命令,如果对里程碑名字不满意的话,可以删除旧的里程碑,然后重新用新的名称创建里程碑。

创建的里程碑默认只在本地版本库中可见,单单使用 git push 也不能将里程碑也推送到远程版本库。如果确实需要将本地建立的里程碑推送到远程版本库,需要在 git push 命令中明确地表示出来,例如将 mytag 里程碑推送到上游版本库:

$ git push origin mytag
Total 0 (delta 0), reused 0 (delta 0)
To file:///path/to/repos/hello-world.git
* [new tag]         mytag -> mytag

将本地建立的所有里程碑全部推送到远程版本库:

$ git push origin refs/tags/*
Counting objects: 2, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 687 bytes, done.
Total 2 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (2/2), done.
To file:///path/to/repos/hello-world.git
* [new tag]         mytag2 -> mytag2
* [new tag]         mytag3 -> mytag3

用远程共享版本库的引用覆盖本地版本库的同名引用:

$ git pull origin refs/tags/mytag2:refs/tags/mytag2
remote: Counting objects: 1, done.
remote: Total 1 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (1/1), done.
From file:///path/to/repos/hello-world
- [tag update]      mytag2     -> mytag2
Already up-to-date.

删除远程版本库的里程碑:

$ git push origin :mytag2
To file:///path/to/repos/hello-world.git
- [deleted]         mytag2

其中,git push <remote_url> :<tagname> 表示将一个空值推送到远程版本库对应的引用中,亦即删除远程版本库中相关的引用。这个命令不但可以用于删除里程碑,还可以用它删除远程版本库中的分支。例如,在执行 git branch -d user2/i18n 删除本地分支之后,还需要执行 git push origin :user2/i18n 将远程分支也同时删除。

出于安全考虑,将远程版本库的配置 receive.denyDeletes 设置为 true,可以禁止删除分支。更好的方法是通过架设基于 SSH 协议的 Git 服务器,配置不同用户的分支删除权限。

引用命名规范

Git的里程碑命名还有一些特殊的约定需要遵守。实际上,下面的这些约定对于分支及任何其他引用均适用:

  • 不能以符号 “-” 开头。以免在命令行中被当成命令的选项。
  • 可以包含路径分隔符 “/”,但是路径分隔符不能位于最后。 使用路径分隔符创建 tag 实际上会在引用目录下创建子目录。例如名为 demo/v1.2.1 的里程碑,就会创建目录 .git/refs/tags/demo 并在该目录下创建引用文件 v1.2.1。
  • 不能出现两个连续的点 “..”。因为两个连续的点被用于表示版本范围,当然更不能使用三个连续的点。
  • 如果在里程碑命名中使用了路径分隔符 “/”,就不能在任何一个分隔路径中以点 “.” 开头。 这是因为里程碑在用简写格式表达时,可能造成以一个点 “.” 开头。这样的引用名称在用作版本范围的最后一个版本时,本来两点操作符变成了三点操作符,从而造成歧义。
  • 不能在里程碑名称的最后出现点 “.” 。否则作为第一个参数出现在表示版本范围的表达式中时,本来版本范围表达式可能用的是两点操作符,结果被误作三点操作符。
  • 不能使用特殊字符,如:空格、波浪线 “~”、脱字符 “^”、冒号 “:”、问号 “?”、星号 “*”、方括号 “[”,以及字符 \177(删除字符)或小于 \040(32)的 ASCII 码都不能使用。 这是因为波浪线 “~” 和脱字符 “^” 都用于表示一个提交的祖先提交。冒号被用作引用表达式来分隔两个不同的引用,或者用于分隔引用代表的树对象和该目录树中的文件。问号、星号和方括号在引用表达式中都被用作通配符。
  • 不能以 “.lock” 为结尾。因为以 “.lock” 结尾的文件是里程碑操作过程中的临时文件。
  • 不能包含 “@{” 字串。否则易和 reflog 的 “@{<num>}” 语法相混淆。
  • 不能包含反斜线 “\”。因为反斜线用于命令行或 shell 脚本会造成意外。

Git 还专门为检查引用名称是否符合规范提供了一个命令: git check-ref-format。若该命令返回值为 0,则引用名称符合规范,若返回值为 1,则不符合规范。

$ git check-ref-format refs/tags/.name || echo "返回 $?,不合法的引用"
返回 1,不合法的引用

分支的类型

从应用的角度,分支有几种不同的类型:发布分支、特性分支和卖主分支。

  • Bugfix 分支或发布分支(Release Branch):在软件新版本发布后用于软件维护,发布升级版本。发布分支上提交的修正代码需要向主线合并。
  • 特性分支(Feature Branch)或主题分支(Topic Branch):用于将某个功能或模块的开发与开发主线独立出来。需要使用特性分支的场景:试验性、探索性的功能开发;功能复杂、开发周期长(有可能在本次发布中取消)的模块;会更改软件体系架构,破坏软件集成,或者容易导致冲突、影响他人开发进度的模块。很多项目在特性分支合并到开发主线的时候,都不推荐使用合并操作,而是使用变基操作。使用变基操作的分支关系图要比采用合并操作的简单得多,看起来更像是集中式版本控制系统特有的顺序提交。
  • 卖主分支(Vendor Branch):在版本库中创建一个专门和上游代码进行同步的分支,一旦有上游代码发布就检入到卖主分支中,然后在主线上合并卖主分支上的新提交。

git cherry 命令

git cherry 命令可用于查看哪些提交未被推送到上游跟踪分支中,在合版本时可使用此命令确认本地提交已推送到版本服务器,例如:

$ git cherry
+ c953bbaab6d77303058d9c68888e0a16acd3d2be
$ git cherry -v
+ c953bbaab6d77303058d9c68888e0a16acd3d2be Fix typo: -help to --help.

[remote] 配置

配置文件 .git/config 中的 [remote] 小节,以 origin 为名注册了一个远程版本库:

[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*
url = file:///path/to/repos/hello-world.git

其中的 fetch 一行,设置了执行 git fetch origin 操作时使用的默认引用表达式:

  • 该引用表达式以加号(+)开头,含义是强制进行引用的替换,即使即将进行的替换是非快进式的。
  • 引用表达式中使用了通配符,冒号前面的含有通配符的引用指的是远程版本库的所有分支,冒号后面的引用含义是复制到本地的远程分支目录中。

当执行 git fetch origin 操作时,就相当于执行了下面的命令,将远程版本库的所有分支复制为本地的远程分支。

$ git fetch origin +refs/heads/*:refs/remotes/origin/*

远程分支不是真正意义上的分支,是类似于里程碑一样的引用。如果针对远程分支执行检出命令,会使得头指针 HEAD 处于分离头指针状态。如果对远程分支进行修改,需要创建新的本地分支。对于 1.6.6 或更新版本的 Git,可使用下面的命令同时完成分支的创建和切换:

$ git checkout hello-1.x

如果 Git 的版本较老,或注册了多个远程版本库,因此存在多个名为 hello-1.x 的远程分支,而需要显式地从远程分支中创建本地分支:

$ git checkout -b hello-1.x origin/hello-1.x

从远程分支创建本地分支时,自动建立了分支间的跟踪,而从一个本地分支创建另外一个本地分支则没有。如果希望在基于一个本地分支创建另外一个本地分支时也能够使用分支间的跟踪功能,就要在创建分支时提供 --track 参数(arc branch local_brach 创建的本地分支就是使用了此项功能)。例如,基于 hello-1.x 创建 hello-jx 分支:

$ git checkout --track -b hello-jx hello-1.x

从 Git 库的配置文件中会看到为 hello-jx 分支设置的跟踪。因为跟踪的是本版本库的本地分支,因此设置的远程版本库名称为一个点。

[branch "hello-jx"]
remote = .
merge = refs/heads/hello-1.x

设置远程版本库

将版本库 file:///path/to/repos/hello-user1.git 以 new-remote 为名进行注册。

$ git remote add new-remote file:///path/to/repos/hello-user1.git

更改远程版本库的地址,可手工修改 .git/config 文件,可使用 git config 命令修改,也可使用 git remote 命令修改,如下:

$ git remote set-url new-remote file:///path/to/repos/hello-user2.git

上述命令同时修改了 git fetch 命令和 git push 命令用到的 URL 地址,也可为 git push 命令设置单独的 URL 地址,只需加上 --push 选项即可:

$ git remote set-url --push new-remote /path/to/repos/hello-user2.git

此时,配置文件 .git/config 的对应 [remote] 小节也会增加一条新的名为 pushurl 的配置。

更改远程版本库的名称,可使用如下命令,将 new-remote 名称修改为 user2:

$ git remote rename new-remote user2

更新所有远程版本库,使用 git remote update 命令。若某个远程版本库不想在执行 git remote update 时获得更新,可通过参数关闭自动更新,例如下面的命令关闭远程版本库 user2 的自动更新:

$ git config remote.user2.skipDefaultUpdate true

删除远程版本库,使用 git remoterm 子命令,例如删除注册的 user2 版本库:

$ git remote rm user2

git pull 命令

在执行 git pull 操作时可以通过参数 --rebase 设置使用变基而非合并操作,将本地分支的改动变基到跟踪分支上。对于特定的分支 <branchname>,可通过如下配置使默认采用变基操作,而不是合并操作:

$ git config branch.<branchname>.rebase true

对于整个本地版本库设置如下参数,则在基于远程分支建立本地追踪分支时,会自动配置 branch.<branchname>.rebase 参数:

$ git config branch.autosetuprebase remote

branch.autosetuprebase 的可能值及其含义如下:

  • never: 不自动设置任何追踪分支的 branch.<branchname>.rebase 参数。
  • local: 为基于本地分支建立的追踪分支设置 branch.<branchname>.rebase 参数。
  • remote: 为基于远程分支建立的追踪分支设置 branch.<branchname>.rebase 参数。
  • always: 为所有追踪分支设置 branch.<branchname>.rebase 参数。

当删除注册的远程版本库时,远程分支会被删除,但是该远程版本库引入的里程碑不会被删除。可以在执行 git fetch 命令时,通过提交 -n--no-tags 参数设置不获取里程碑只获取分支及提交:

$ git fetch --no-tags file:///path/to/repos/hello-world.git refs/heads/*:refs/remotes/hello-world/*

在注册远程版本库时,也可使用 --no-tags 参数,避免将远程版本库的里程碑引入本地版本库:

$ git remote add --no-tags hell-world file:///path/to/repos/hello-world.git

git am 命令

使用 git am 命令可以使得保存在 mbox 中的邮件批量地应用在版本库中。am 是 apply email 的缩写。

// 进入 Linux mail 邮箱,注意后继的命令行提示符的变化
$ mail
// 将第 1-3 封邮件另存为 user1-mail-archive
& s 1-3 user1-mail-archive
// 退出 mail 邮箱
& q
// 将 mbox 格式的 user1-mail-archive 应用到版本库中
$ git am user1-mail-archive

git am 命令既可识别 mbox 格式的文件,也可识别 .patch 格式的文件。例如,使用 git format-patch 生成的补丁文件,通过管道符调用 git am 命令应用补丁:

$ ls *.patch
0001-Fix-typo-help-to-help.patch  0002-Add-I18N-support.patch  0003-Translate-for-Chinese.patch
$ cat *.patch | git am
Applying: Fix typo: -help to --help.
Applying: Add I18N support.
Applying: Translate for Chinese.

StGit 和 Quilt

StGit 是 Stacked Git 的简称,可方便地管理 Git 补丁,在设计上参考了 Quilt,并且可以输出 Quilt 兼容的补丁列表。StGit 的工作流程如下:

// 进入版本库
$ cd stgit-demo
// 在当前工作区初始化 StGit
$ stg init
// 查看当前补丁列表
$ stg series
// 将最新的三个提交转换为 StGit 补丁
$ stg uncommit -n 3
Uncommitting 3 patches ...
Now at patch "translate-for-chinese"
done
// 再次查看当前补丁列表,stg series 可简写为 stg ser
// 加号(+)代表该补丁已经应用在版本库中,大于号(>)用于标识当前的补丁。
$ stg ser
+ fix-typo-help-to-help
+ add-i18n-support
> translate-for-chinese
// 执行补丁出栈的命令,将补丁撤出应用,使用 `-a` 参数可将所有补丁撤出应用
$ stg pop
Popped translate-for-chinese
Now at patch "add-i18n-support"
$ stg pop -a
Popped add-i18n-support -- fix-typo-help-to-help
No patch applied
// 再次查看当前补丁列表,会看到每个补丁前都用减号(-)标识。
$ stg ser
- fix-typo-help-to-help
- add-i18n-support
- translate-for-chinese
// 执行补丁入栈,即应用补丁
$ stg push
Pushing patch "fix-typo-help-to-help" ... done (unmodified)
Now at patch "fix-typo-help-to-help"
$ stg goto add-i18n-support
Pushing patch "add-i18n-support" ... done (unmodified)
Now at patch "add-i18n-support"
// 修改本地文件,即修复补丁中的错误
$ vim hello.c
// 不要提交,而是执行 `stg refresh` 命令更新补丁
$ stg refresh
// 全部补丁都修复并 push 之后,使用 `stg export` 命令导出 Quilt 格式的补丁集到 patches 目录
$ stg export -d patches
Checking for changes in the working directory ... done
// 查看导出补丁的目标目录
$ ls patches/
add-i18n-support  fix-typo-help-to-help  **series**  translate-for-chinese
// 其中的 series 文件是补丁文件的列表,列在前面的补丁先被应用
$ cat patches/series
# This series applies on GIT commit d81896e60673771ef1873b27a33f52df75f70515
fix-typo-help-to-help
add-i18n-support
translate-for-chinese

在执行 stg export 命令时,导致的补丁文件默认不带数字前缀,可添加 -n 参数为补丁文件添加数字前缀。

Quilt 约定俗成将补丁集放在项目根目录下的子目录 patches 中,否则需要通过环境变量 QUILT_PATCHES 对路径进行设置。Quilt 的工作流程如下:

// 进入版本库
$ cd stgit-demo
// 显示补丁列表,使用了前面已导出到 patches 目录下的补丁文件
$ quilt series
01-fix-typo-help-to-help
02-add-i18n-support
03-translate-for-chinese
// 应用一个补丁
$ quilt push
Applying patch 01-fix-typo-help-to-help
patching file src/main.c
Now at patch 01-fix-typo-help-to-help
// 查看下一个补丁是什么
$ quilt next
02-add-i18n-support
// 应用全部补丁
$ quilt push -a

Git 提供了一个名为 git quiltimport 命令,可以非常方便地将 Quilt 格式的补丁集转化为一个一个的 Git 提交,是 git am 命令的补充。例如要将位于 patches 目录下的 Quilt 补丁集应用到版本库中,可执行下面的命令:

$ git quiltimport