Git用户手札

Posted by Dorck on September 13, 2022

Git 的由来

Git 的诞生有偶然性,也有历史的必然。一方面,随着 Linux 内核开源项目大量维护成本集中在提交补丁和保存归档的繁琐事务上(1991-2002年间); 另一方面,2005 年,Linux 内核项目使用的 BitKeeper 版本控制工具授权被收回。 这就迫使 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds)基于以往经验来开发出一款自己的版本控制系统。 他们对新的系统制订了若干目标:

  • 速度
  • 简单的设计
  • 对非线性开发模式的强力支持(允许成千上万个并行开发的分支)
  • 完全分布式
  • 有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量)

自诞生于 2005 年以来,Git 日臻成熟完善,在高度易用的同时,仍然保留着初期设定的目标。 它的速度飞快,极其适合管理大项目,有着令人难以置信的非线性分支管理系统。

Git 是什么

从概念上来说,其它大部分系统以文件变更列表的方式存储信息,这类系统(CVS、Subversion、Perforce、Bazaar 等等) 将它们存储的信息看作是一组基本文件和每个文件随时间逐步累积的差异(通常被称作 基于差异(delta-based) 的版本控制)。

deltas-based-version-control

Figure 1. 存储每个文件与初始版本的差异.

相比于传统的 CVS、Subversion 和 Perforce 等版本控制工具,Git 能够做到一枝独秀来源于它独特的数据处理方式:直接记录快照,而非差异比较。

snapshots-version-control

Figure 2. 存储项目随时间改变的快照.

Git 更像是把数据看作是对小型文件系统的一系列快照。 在 Git 中,每当你提交更新或保存项目状态时,它基本上就会对当时的全部文件创建一个快照并保存这个快照的索引。 为了效率,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 快照流

在 Git 中,几乎所有操作都是本地执行的。Git 中的绝大多数操作都只需要访问本地文件和资源,一般不需要来自网络上其它计算机的信息。我们本地磁盘就具有项目的完整历史,所以无需再基于可靠网络去服务器拉取历史记录,直接从本地数据库读取即可,这显然要快得多。

Git 能够保证数据完整性。Git 中所有的数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 这个功能建构在 Git 底层,是构成 Git 哲学不可或缺的部分。Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串,基于 Git 中文件的内容或目录结构计算出来。实际上,Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。

最后,Git 一般只添加数据。 你很难使用 Git 从数据库中删除数据,也就是说 Git 几乎不会执行任何可能导致文件不可恢复的操作。 同其他类似 VCS 一样,未提交更新时有可能丢失或弄乱修改的内容。但是一旦你提交快照到 Git 中, 就难以再丢失数据,特别是如果你定期的推送数据库到其它仓库的情况。

Git 的三种状态
  • 已修改 [modified]:表示修改了文件,但还没保存到数据库中。
  • 已暂存 [staged]:表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。

  • 已提交 [committed]:表示数据已经安全地保存在本地数据库中。

相应地会让我们的 Git 项目拥有三个阶段:工作区、暂存区以及 Git 目录。

git-areas

Figure 3. 工作目录、暂存区域以及 Git 仓库.

基本的 Git 工作流程如下:

  1. 在工作区 (本地项目文件) 中修改文件。
  2. 将你想要下次提交的更改选择性地暂存,这样只会将更改的部分添加到暂存区 (索引文件)。
  3. 提交更新,找到暂存区的文件,将快照永久性存储到 Git 目录 (Git 数据库)。

安装 Git

在 Linux 上安装

如果你想在 Linux 上用二进制安装程序来安装基本的 Git 工具,可以使用发行版包含的基础软件包管理工具来安装。 以 Fedora 为例,如果你在使用它(或与之紧密相关的基于 RPM 的发行版,如 RHEL 或 CentOS),你可以使用 dnf

1
$ sudo dnf install git-all

如果你在基于 Debian 的发行版上,如 Ubuntu,请使用 apt

1
$ sudo apt install git-all

要了解更多选择,Git 官方网站上有在各种 Unix 发行版的系统上安装步骤,网址为 https://git-scm.com/download/linux。

在 macOS 上安装

在 Mac 上安装 Git 有多种方式。 最简单的方法是安装 Xcode Command Line Tools。 Mavericks (10.9) 或更高版本的系统中,在 Terminal 里尝试首次运行 git 命令即可。

1
$ git --version

如果没有安装过命令行开发者工具,将会提示你安装。如果你想安装更新的版本,可以使用二进制安装程序。 官方维护的 macOS Git 安装程序可以在 Git 官方网站下载,网址为 https://git-scm.com/download/mac。

在 Windows 上安装

在 Windows 上安装 Git 也有几种安装方法。 官方版本可以在 Git 官方网站下载。 打开 https://git-scm.com/download/win,下载会自动开始。 要注意这是一个名为 Git for Windows 的项目(也叫做 msysGit),和 Git 是分别独立的项目;更多信息请访问 http://msysgit.github.io/。

Git 基本用法

以下涉及到 Git 的基本用法,基于命令行工具操作,毕竟只有她才能发挥 Git 的十成功力。

Git config

Git 自带 git config 的工具来帮助设置控制 Git 外观和行为的配置变量。 这些变量存储在三个不同的位置:

  • /etc/gitconfig 文件: 包含系统上每一个用户及他们仓库的通用配置。 如果在执行 git config 时带上 --system 选项,那么它就会读写该文件中的配置变量(需要超级管理员权限)。

  • ~/.gitconfig~/.config/git/config 文件:只针对当前用户。 你可以传递 --global 选项让 Git 读写此文件,这会对你系统上所有的仓库生效。

  • 当前使用仓库的 Git 目录中的 config 文件(即 .git/config):针对该仓库。 你可以传递 --local 选项让 Git 强制读写此文件,虽然默认情况下用的就是它。。 (当然,你需要进入某个 Git 仓库中才能让该选项生效。)

每一个级别会覆盖上一级别的配置,例如 .git/config 的配置变量会覆盖 /etc/gitconfig 中的配置变量。我们可以通过以下命令查看所有的配置以及它们所在的文件:

1
$ git config --list --show-origin
配置用户信息

安装完 Git 之后,要做的第一件事就是设置你的用户名和邮件地址。 这一点很重要,因为每一个 Git 提交都会使用这些信息,它们会写入到你的每一次提交中,不可更改:

1
2
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

如果使用了 --global 选项,那么该命令只需要运行一次,因为之后无论你在该系统上做任何事情, Git 都会使用那些信息。 如果只想作用于本地某个项目,只需要执行 --local 即可。

配置文本编辑器

默认情况下,Git 会使用 vim 作为其默认的文本编辑器,但不得不说,它的各方面体验还是不太习惯。我们同样可以通过 git config 命令来配置它:

1
$ git config --global core.editor emacs

如果你想了解如何将 VsCode 作为 Git 默认编辑器,那么可以参考我之前的一篇文章 » Git 默认编辑器替换

查看配置信息

如果想要查看当前 Git 配置信息,可以使用 git config --list 命令来列出所有 Git 当时能找到的配置信息:

1
2
3
4
5
6
7
8
$ git config --list
user.name=Petter
user.email=Petter@gmail.com
color.status=auto
color.branch=auto
color.interactive=auto
color.diff=auto
...

你可能会看到重复的变量名,因为 Git 会从不同的文件中读取同一个配置(例如:/etc/gitconfig~/.gitconfig)。 这种情况下,Git 会使用它找到的每一个变量的最后一个配置。

你可以通过输入 git config <key>: 来查看 Git 某一项配置信息:

1
2
$ git config user.name
Petter

Git help

若你使用 Git 时需要获取帮助,有三种等价的方法可以找到 Git 命令的综合手册(manpage):

1
2
3
$ git help <option>
$ git <option> --help
$ man git-<option>

例如,要想获得 git config 命令的详细操作手册,可以执行:

1
$ git help config

当然,也可以执行 -h 来获得简单的帮助信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ git config -h
usage: git config [<options>]

Config file location
    --global              use global config file
    --system              use system config file
    --local               use repository config file
    --worktree            use per-worktree config file
    -f, --file <file>     use given config file
    --blob <blob-id>      read config from given blob object

Action
    --get                 get value: name [value-pattern]
    --get-all             get all values: key [value-pattern]
    --get-regexp          get values for regexp: name-regex [value-pattern]
    --get-urlmatch        get value specific for the URL: section[.var] URL
    --replace-all         replace all matching variables: name value [value-pattern]
    --add                 add a new variable: name value
    --unset               remove a variable: name [value-pattern]
    --unset-all           remove all matches: name [value-pattern]
    --rename-section      rename section: old-name new-name
    --remove-section      remove a section: name
    -l, --list            list all
    --fixed-value         use string equality when comparing values to 'value-pattern'
    -e, --edit            open an editor
    --get-color           find the color configured: slot [default]
    --get-colorbool       find the color setting: slot [stdout-is-tty]

Type
    -t, --type <>         value is given this type
    --bool                value is "true" or "false"
    --int                 value is decimal number
    --bool-or-int         value is --bool or --int
    --bool-or-str         value is --bool or string
    --path                value is a path (file or directory name)
    --expiry-date         value is an expiry date

Other
    -z, --null            terminate values with NUL byte
    --name-only           show variable names only
    --includes            respect include directives on lookup
    --show-origin         show origin of config (file, standard input, blob, command line)
    --show-scope          show scope of config (worktree, local, global, system, command)
    --default <value>     with --get, use default value when missing entry

获取 Git 仓库

通常有两种获取 Git 项目仓库的方式:

  • 将尚未进行版本控制的本地目录转换为 Git 仓库;

  • 从其它服务器克隆一个已存在的 Git 仓库。

在已存在目录中初始化仓库

首先进入本地项目所在目录:

1
2
3
4
5
6
#在 Linux 上
$ cd /home/user/my_project 
#在 macOS 上
$ cd /Users/user/my_project
#在 Windows 上
$ cd /c/user/my_project

之后执行:

1
$ git init

该命令将创建一个名为 .git 的子目录,这个目录包含初始化的 Git 仓库中所有必须文件。

最后,执行以下命令即可将项目快照保存到 Git 数据库中,即项目已被 Git 成功追踪:

1
2
$ git add .
$ git commit -m 'initial project version'
克隆已有仓库

克隆仓库的命令是 git clone <url>,如:

1
$ git clone https://github.com/libgit2/libgit2

该命令会在当前目录下创建一个名为 “libgit2” 的目录,并在这个目录下初始化一个 .git 文件夹, 从远程仓库拉取下所有数据放入 .git 文件夹,然后从中读取最新版本的文件的拷贝。

如果你想在克隆远程仓库的时候,自定义本地仓库的名字,你可以通过额外的参数指定新的目录名:

1
$ git clone https://github.com/libgit2/libgit2 mylibgit

这会执行与上一条命令相同的操作,但目标目录名变为了 mylibgit

Git 支持多种数据传输协议。 上面的例子使用的是 https:// 协议,不过你也可以使用 git:// 协议或者使用 SSH 传输协议,比如 user@server:path/to/repo.git在服务器上搭建 Git 将会介绍所有这些协议在服务器端如何配置使用,以及各种方式之间的利弊。

更新记录到仓库

接下来涉及到如何对已修改文件进行暂存和提交。

git-lifecycle

Figure 4. 文件的状态变化周期

工作目录下的每一个文件都不外乎这两种状态:已跟踪未跟踪。 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后,它们的状态可能是未修改,已修改或已放入暂存区。简而言之,已跟踪的文件就是 Git 已经知道的文件。工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有被放入暂存区。

查看当前工作区状态

我们在项目下新建一个新的 README 文件。 如果之前并不存在这个文件,使用 git status 命令,你将看到一个新的未跟踪文件:

1
2
3
4
5
6
7
8
9
10
$ echo 'My Project' > README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    README

nothing added to commit but untracked files present (use "git add" to track)

可以看到新建的 README 文件出现在 Untracked files 下面。 未跟踪的文件意味着 Git 在之前的快照(提交)中没有这些文件;Git 不会自动将之纳入跟踪范围,除非你明明白白地告诉它“我需要跟踪该文件”。

git status 命令的输出十分详细,但其用语有些繁琐。 Git 有一个选项可以帮你缩短状态命令的输出,这样可以以简洁的方式查看更改。 如果你使用 git status -s 命令或 git status --short 命令,你将得到一种格式更为紧凑的输出。

1
2
3
4
5
6
$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

新添加的未跟踪文件前面有 ?? 标记,新添加到暂存区中的文件前面有 A 标记,修改过的文件前面有 M 标记。 输出中有两栏,左栏指明了暂存区的状态,右栏指明了工作区的状态。例如,上面的状态报告显示: README 文件在工作区已修改但尚未暂存,而 lib/simplegit.rb 文件已修改且已暂存。 Rakefile文件已修改,暂存后又作了修改,因此该文件的修改中既有已暂存的部分,又有未暂存的部分。

将文件加入暂存区

使用命令 git add 开始跟踪一个新文件。 所以,要跟踪 README 文件,运行:

1
$ git add README

此时再运行 git status 命令,会看到 README 文件已被跟踪,并处于暂存状态:

1
2
3
4
5
6
7
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

    new file:   README

git add 是个多功能命令:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态等。 将这个命令理解为“精确地将内容添加到下一次提交中”而不是“将一个文件添加到项目中”要更加合适。

忽略文件

通常有些文件既无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件的模式。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Log/OS Files
*.log

# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json

# IntelliJ
*.iml
.idea/

# Keystore files
*.jks
*.keystore

.gitignore 的文件格式规范如下:

  • 所有空行或者以 # 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
  • 匹配模式可以 / 开头防止递归。
  • 匹配模式可以 / 结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上叹号 ! 取反。

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。 星号 * 匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符 (这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c ); 问号 ? 只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符, 表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。 使用两个星号 ** 表示匹配任意中间目录,比如 a/**/z 可以匹配 a/za/b/za/b/c/z 等。

GitHub 有一个十分详细的针对数十种项目及语言的 .gitignore 文件列表, 可以在 https://github.com/github/gitignore 找到它。

列举所有修改项

如果觉得 git status 信息太简略,那么可以执行 git diff 来查看具体修改信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
 Please include a nice description of your changes when you submit your PR;
 if we have to read the whole diff to figure out why you're contributing
 in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.

 If you are starting to work on a particular area, feel free to submit a PR
 that highlights your work in progress (and note in the PR title that it's

此命令比较的是工作目录中当前文件和暂存区域快照之间的差异。 也就是修改之后还没有暂存起来的变化内容。也可以用 git diff --staged 或者 git diff --cached 命令来比对已暂存文件与最后一次提交的文件差异

1
2
3
4
5
6
7
8
$ git diff --staged
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+My Project
提交更新

暂存区有了新成员加入,我们可以通过 git commit 命令来提交更新:

1
2
3
4
$ git commit -m "Story 182: Fix benchmarks for speed"
[master 463dc4f] Story 182: Fix benchmarks for speed
 2 files changed, 2 insertions(+)
 create mode 100644 README

也可以直接执行 git commit 来通过打开 git 默认文本编辑器来进行输入提交信息(一般默认为 vim):

1
2
3
4
5
6
7
8
9
10
11
12
13
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
#	new file:   README
#	modified:   CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C

你也可以尝试更换默认文本编辑器,参考: Git 默认编辑器替换为Vscode

Git 提供了一个跳过使用暂存区域的方式, 只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤:

1
2
3
4
$ git commit -a -m 'added new benchmarks'
[master 83e38c7] added new benchmarks
 1 file changed, 5 insertions(+), 0 deletions(-)

但这个命令也是有副作用的,它会将所有变更的文件都提交。

移除文件

如果我们希望将本地文件 NEW.txt 删除,并且不希望出现在暂存区,那么可以使用 rm NEW.txt & git rm NEW.txt,等同于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ rm NEW.txt 
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   CHANGE.txt
	new file:   NEW.txt
	new file:   README.md

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	deleted:    NEW.txt

$ git rm NEW.txt
rm 'NEW.txt'
$ git status    
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   CHANGE.txt
	new file:   README.md

这样,暂存区和工作区就好像没存在过 NEW.txt 文件一样了。当然,git 也提供了强制从暂存区删除及磁盘删除文件的命令 -f

1
$ git rm New.txt -f

不过不太推荐如此用,这是一种危险操作,这样的数据删除后不能被 Git 恢复。

如果仅希望文件从 git 的暂存区移除,即让文件保留在磁盘,但是并不想让 Git 继续跟踪, 可以使用 --cached 选项:

1
$ git rm --cached README

如果不小心把某些编译生成文件添加到暂存区时,这一做法尤其有用。 git rm 命令后面可以列出文件或者目录的名字,也可以使用 glob 模式。比如:

1
$ git rm log/\*.log

注意到星号 * 之前的反斜杠 \, 因为 Git 有它自己的文件模式扩展匹配方式,所以我们不用 shell 来帮忙展开。 此命令删除 log/ 目录下扩展名为 .log 的所有文件。 类似的比如:

1
$ git rm \*~

该命令会删除所有名字以 ~ 结尾的文件。

移动文件

如果我们对暂存区中文件改名,执行 git status 可以发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   CHANGE.txt
	new file:   NEW.txt
	new file:   README.md

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	deleted:    NEW.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	OLD.txt

Git 推断不出这是一次改名操作,而是认为先删除了原文件又新增了另一个文件。我们可以借助于 git mv 命令来让 git 识别出这一操作:

1
2
3
4
5
6
7
8
$ git mv README.md README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README
查看提交历史

我们可以通过 git log 命令查看提交历史:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git log
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test

commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 10:31:28 2008 -0700

    first commit

不传入任何参数的默认情况下,git log 会按时间先后顺序列出所有的提交,最近的更新排在最上面。 正如你所看到的,这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明。

git log 还支持众多常用额外参数:

选项 说明
-p 按补丁格式显示每个提交引入的差异,后面可跟随数字来查看提交数量,如:git log -p -2
--stat 显示每次提交的文件修改统计信息。
--shortstat 只显示 --stat 中最后的行数修改添加移除统计。
--name-only 仅在提交信息后显示已修改的文件清单。
--name-status 显示新增、修改、删除的文件清单。
--abbrev-commit 仅显示 SHA-1 校验和所有 40 个字符中的前几个字符。
--relative-date 使用较短的相对时间而不是完整格式显示日期(比如:2 weeks ago)。
--graph 在日志旁以 ASCII 图形显示分支与合并历史。
--pretty 使用其他格式显示历史提交信息。可用的选项包括 oneline、short、full、fuller 和 format(用来定义自己的格式)。
--oneline --pretty=oneline --abbrev-commit 合用的简写。

其中一个比较有用的选项是 -p--patch ,它会显示每次提交所引入的差异(按 补丁 的格式输出)。 你也可以限制显示的日志条目数量,例如使用 -2 选项来只显示最近的两次提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ git log -p -2
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

diff --git a/Rakefile b/Rakefile
index a874b73..8f94139 100644
--- a/Rakefile
+++ b/Rakefile
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
 spec = Gem::Specification.new do |s|
     s.platform  =   Gem::Platform::RUBY
     s.name      =   "simplegit"
-    s.version   =   "0.1.0"
+    s.version   =   "0.1.1"
     s.author    =   "Scott Chacon"
     s.email     =   "schacon@gee-mail.com"
     s.summary   =   "A simple gem for using Git in Ruby code."

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test

diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index a0a60ae..47c6340 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -18,8 +18,3 @@ class SimpleGit
     end

 end
-
-if $0 == __FILE__
-  git = SimpleGit.new
-  puts git.show
-end

该选项除了显示基本信息之外,还附带了每次提交的变化。 当进行代码审查,或者快速浏览某个搭档的提交所带来的变化的时候,这个参数就非常有用了。

另一个非常有用的选项是 --pretty。 这个选项可以使用不同于默认格式的方式展示提交历史。最有意思的选项是 format ,可以定制记录的显示格式。 这样的输出对后期提取分析格外有用——因为你知道输出的格式不会随着 Git 的更新而发生改变:

1
2
3
4
$ git log --pretty=format:"%h - %an, %ar : %s"
ca82a6d - Scott Chacon, 6 years ago : changed the version number
085bb3b - Scott Chacon, 6 years ago : removed unnecessary test
a11bef0 - Scott Chacon, 6 years ago : first commit

更多 format 常用选项参考:Git log pretty format

另外,我们也可以设置 log 输出的条目数量,类似 --since--until 这种按照时间作限制的选项很有用。 例如,下面的命令会列出最近两周的所有提交:

1
$ git log --since=2.weeks

该命令可用的格式十分丰富——可以是类似 "2008-01-15" 的具体的某一天,也可以是类似 "2 years 1 day 3 minutes ago" 的相对日期。还可以过滤出匹配指定条件的提交。 用 --author 选项显示指定作者的提交,用 --grep 选项搜索提交说明中的关键字。

以下是支持的常用过滤维度:

选项 说明
-<n> 仅显示最近的 n 条提交。
--since, --after 仅显示指定时间之后的提交。
--until, --before 仅显示指定时间之前的提交。
--author 仅显示作者匹配指定字符串的提交。
--committer 仅显示提交者匹配指定字符串的提交。
--grep 仅显示提交说明中包含指定字符串的提交。
-S 仅显示添加或删除内容匹配指定字符串的提交。

如果要在 Git 源码库中查看 Junio Hamano 在 2008 年 10 月其间, 除了合并提交之外的哪一个提交修改了测试文件,可以使用下面的命令:

1
2
3
4
5
6
7
8
$ git log --pretty="%h - %s" --author='Junio C Hamano' --since="2008-10-01" \
   --before="2008-11-01" --no-merges -- t/
5610e3b - Fix testcase failure when extended attributes are in use
acd3b9e - Enhance hold_lock_file_for_{update,append}() API
f563754 - demonstrate breakage of detached checkout with symbolic link HEAD
d1a43f2 - reset --hard/read-tree --reset -u: remove unmerged new paths
51a94af - Fix "checkout --track -b newbranch" on detached HEAD
b0ad11e - pull: allow "git pull origin $something:$current_branch" into an unborn branch

在近 40000 条提交中,上面的输出仅列出了符合条件的 6 条记录。

撤销操作

使用 git commit --amend 补充提交信息,覆盖上一次的提交内容,可以像下面这样操作:

1
2
3
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend

最终你只会有一个提交,即第二次提交内容将代替第一次提交的结果。该命令适合在提交完了才发现漏掉了几个文件没有添加或者提交信息写错了的情况来使用。

如果不小心暂存了某个文件,我们可以借助于 git reset HEAD <file> 来撤销暂存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M	CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

如何撤销对未暂存文件的修改?只需要执行:

1
2
3
4
5
6
7
$ git checkout -- CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README
远程仓库使用

如果想查看你已经配置的远程仓库服务器,可以运行 git remote 命令。 它会列出你指定的每一个远程服务器的简写。

1
2
$ git remote
origin

也可以指定选项 -v,会显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL:

1
2
3
$ git remote -v
origin	https://github.com/schacon/ticgit (fetch)
origin	https://github.com/schacon/ticgit (push)

如果想要查看某一个远程仓库的更多信息,可以使用 git remote show <remote> 命令。 如果想以一个特定的缩写名运行这个命令,例如 origin,会得到像下面类似的信息:

1
2
3
4
5
6
7
8
9
10
11
12
$ git remote show origin
* remote origin
  Fetch URL: https://github.com/schacon/ticgit
  Push  URL: https://github.com/schacon/ticgit
  HEAD branch: master
  Remote branches:
    master                               tracked
    dev-branch                           tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

当然也可以通过以下命令添加远程仓库,运行 git remote add <shortname> <url> 添加一个新的远程 Git 仓库,同时指定一个方便使用的简写:

1
2
3
4
5
6
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin	https://github.com/schacon/ticgit (fetch)
origin	https://github.com/schacon/ticgit (push)
pb	https://github.com/paulboone/ticgit (fetch)
pb	https://github.com/paulboone/ticgit (push)

从远程仓库中获得数据,可以执行:

1
$ git fetch <remote>

这个命令会访问远程仓库,从中拉取所有你还没有的数据。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。可以用 git pull 命令来拉取&合并该远程分支到当前分支。

当你想分享你的项目时,必须将其推送到上游。 这个命令很简单:git push <remote> <branch>,如:

1
$ git push origin master

可以运行 git remote rename <oldName> <newName> 来修改一个远程仓库的简写名:

1
2
3
4
$ git remote rename pb paul
$ git remote
origin
paul

如果因为一些原因想要移除一个远程仓库——你已经从服务器上搬走了或不再想使用某一个特定的镜像了, 又或者某一个贡献者不再贡献了——可以使用 git remote removegit remote rm

1
2
3
$ git remote remove paul
$ git remote
origin

一旦你使用这种方式删除了一个远程仓库,那么所有和这个远程仓库相关的远程跟踪分支以及配置信息也会一起被删除。

设置标签

像其他版本控制系统(VCS)一样,Git 可以给仓库历史中的某一个提交打上标签,以示重要。 比较有代表性的是人们会使用这个功能来标记发布结点( v1.0v2.0 等等)。

列出已有标签只需要输入 git tag (可带上可选的 -l 选项 --list):

1
2
3
$ git tag
v1.0
v2.0

如果标签较多也可借助通配符过滤:

1
2
3
4
5
6
$ git tag -l "v1.8.5*"
v1.8.5
v1.8.5-rc0
v1.8.5-rc1
v1.8.5-rc2
v1.8.5-rc3

Git 支持两种标签:轻量标签(lightweight)与附注标签(annotated)。轻量标签很像一个不会改变的分支——它只是某个特定提交的引用。而附注标签是存储在 Git 数据库中的一个完整对象, 它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息,并且可以使用 GNU Privacy Guard (GPG)签名并验证。 通常会建议创建附注标签,这样你可以拥有以上所有信息。但是如果你只是想用一个临时的标签, 或者因为某些原因不想要保存这些信息,那么也可以用轻量标签。

在 Git 中创建附注标签十分简单。 最简单的方式是当你在运行 tag 命令时指定 -a 选项:

1
2
3
4
5
$ git tag -a v1.4 -m "my version 1.4"
$ git tag
v0.1
v1.3
v1.4

-m 选项指定了一条将会存储在标签中的信息。 如果没有为附注标签指定一条信息,Git 会启动编辑器要求你输入信息。

通过使用 git show 命令可以看到标签信息和与之对应的提交信息:

1
2
3
4
5
6
7
8
9
10
11
12
$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date:   Sat May 3 20:19:12 2014 -0700

my version 1.4

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

输出显示了打标签者的信息、打标签的日期时间、附注信息,然后显示具体的提交信息。

轻量标签本质上是将提交校验和存储到一个文件中——没有保存任何其他信息。 创建轻量标签,不需要使用 -a-s-m 选项,只需要提供标签名字:

1
2
3
4
5
6
7
$ git tag v1.4-lw
$ git tag
v0.1
v1.3
v1.4
v1.4-lw
v1.5

这时,如果在标签上运行 git show,你不会看到额外的标签信息。 命令只会显示出提交信息:

1
2
3
4
5
6
$ git show v1.4-lw
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

我们也可以对过去的提交打标签:

1
$ git tag -a v1.2 9fceb02

在创建完标签后你必须显式地推送标签到共享服务器上。 这个过程就像共享远程分支一样——你可以运行 git push origin <tagname>,如:

1
2
3
4
5
6
7
8
$ git push origin v1.5
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
 * [new tag]         v1.5 -> v1.5

如果想要一次性推送很多标签,也可以使用带有 --tags 选项的 git push 命令。 这将会把所有不在远程仓库服务器上的标签全部传送到那里。

要删除掉你本地仓库上的标签,可以使用命令 git tag -d <tagname>。 例如,可以使用以下命令删除一个轻量标签:

1
2
$ git tag -d v1.4-lw
Deleted tag 'v1.4-lw' (was e7d5add)

注意上述命令并不会从任何远程仓库中移除这个标签,你必须用 git push <remote> :refs/tags/<tagname> 来更新你的远程仓库:

第一种变体是 git push <remote> :refs/tags/<tagname>

1
2
3
$ git push origin :refs/tags/v1.4-lw
To /git@github.com:schacon/simplegit.git
 - [deleted]         v1.4-lw

上面这种操作的含义是,将冒号前面的空值推送到远程标签名,从而高效地删除它。

第二种更直观的删除远程标签的方式是:

1
$ git push origin --delete <tagname>

如果你想查看某个标签所指向的文件版本,可以使用 git checkout 命令, 虽然这会使你的仓库处于“分离头指针(detached HEAD)”的状态——这个状态有些不好的副作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git checkout 2.0.0
Note: checking out '2.0.0'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch>

HEAD is now at 99ada87... Merge pull request #89 from schacon/appendix-final

$ git checkout 2.0-beta-0.1
Previous HEAD position was 99ada87... Merge pull request #89 from schacon/appendix-final
HEAD is now at df3f601... add atlas.json and cover image

在“分离头指针”状态下,如果你做了某些更改然后提交它们,标签不会发生变化, 但你的新提交将不属于任何分支,并且将无法访问,除非通过确切的提交哈希才能访问。 因此,如果你需要进行更改,比如你要修复旧版本中的错误,那么通常需要创建一个新分支:

1
2
$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'

如果在这之后又进行了一次提交,version2 分支就会因为这个改动向前移动, 此时它就会和 v2.0.0 标签稍微有些不同。

Git 别名

Git 并不会在你输入部分命令时自动推断出你想要的命令。 如果不想每次都输入完整的 Git 命令,可以通过 git config 文件来轻松地为每一个命令设置一个别名。 这里有一些例子你可以试试:

1
2
3
4
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

这意味着,当要输入 git commit 时,只需要输入 git ci。 随着你继续不断地使用 Git,可能也会经常使用其他命令,所以创建别名时不要犹豫。

在创建你认为应该存在的命令时这个技术会很有用。 例如,为了解决取消暂存文件的易用性问题,可以向 Git 中添加你自己的取消暂存别名:

1
$ git config --global alias.unstage 'reset HEAD --'

这会使下面的两个命令等价:

1
2
$ git unstage fileA
$ git reset HEAD -- fileA

这样看起来更清楚一些。 通常也会添加一个 last 命令,像这样:

1
$ git config --global alias.last 'log -1 HEAD'

这样,可以轻松地看到最后一次提交:

1
2
3
4
5
6
7
8
$ git last
commit 66938dae3329c7aebe598c2246a8e6af90d04646
Author: Josh Goebel <dreamer3@example.com>
Date:   Tue Aug 26 19:48:51 2008 +0800

    test for current head

    Signed-off-by: Scott Chacon <schacon@example.com>

可以看出,Git 只是简单地将别名替换为对应的命令。

分支管理

典型的创建分支场景:有个很紧急的线上问题需要修复,一般将按照如下方式来处理:

  1. 切换到你的线上分支;
  2. 为这个紧急任务新建一个分支,并在其中修复它;
  3. 在测试通过之后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支;
  4. 切换回你最初工作的分支上,继续工作。

首先,我们需要基于当前 master 发布分支新建一个 hotfix 分支:

1
2
$ git checkout -b hotfix/v1.0.1_NPE
Switched to a new branch 'hotfix/v1.0.1_NPE'

接下来修改代码本地测试通过后推送到远程仓库:

1
2
3
4
$ git commit -a -m 'fixed the NPE bug'
[hotfix 1fb7853] fixed the NPE bug
 1 file changed, 2 insertions(+)
$ git push origin hotfix/v1.0.1_NPE 

修复分支构建&测试通过后,可以将 hotfix 分支合并回你的 master 分支来部署到线上。 你可以使用 git merge 命令来达到上述目的:

1
2
3
4
5
6
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

在合并的时候,你应该注意到了“快进(fast-forward)”这个词。 由于你想要合并的分支 hotfix 所指向的提交 C4 是你所在的提交 C2 的直接后继, 因此 Git 会直接将指针向前移动。换句话说,当你试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。

假如我们想将下面的 iss53 分支合并到 master,情况与上面的 fast-forward 策略不太一样了。

basic-branching-6

这和你之前合并 hotfix 分支的时候看起来有一点不一样。在这种情况下,你的开发历史从一个更早的地方开始分叉开来(diverged)。 因为,master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4C5)以及这两个分支的公共祖先(C2),做一个简单的三方合并。

1
2
3
4
5
6
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

basic-merging-2

一般修复分支在合并后可以将其删除:

1
$ git branch -d iss53
解决合并冲突

有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没法干净的合并它们。 如果你对 #53 问题的修改和有关 hotfix 分支的修改都涉及到同一个文件的同一处,在合并它们的时候就会产生合并冲突:

1
2
3
4
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。 你可以在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件:

1
2
3
4
5
6
7
8
9
10
11
$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。 Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:

1
2
3
4
5
6
7
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

这表示 HEAD 所指示的版本(也就是你的 master 分支所在的位置,因为你在运行 merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 的下半部分。 为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。 例如,你可以通过把这段内容换成下面的样子来解决冲突:

1
2
3
<div id="footer">
please contact us at email.support@github.com
</div>

上述的冲突解决方案仅保留了其中一个分支的修改,并且 <<<<<<< , ======= , 和 >>>>>>> 这些行被完全删除了。 在你解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。 一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决。

如果你想使用图形化工具来解决冲突,你可以运行 git mergetool,该命令会为你启动一个合适的可视化合并工具,并带领你一步一步解决这些冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

如果你想使用除默认工具(在这里 Git 使用 opendiff 做为默认的合并工具,因为作者在 Mac 上运行该程序) 外的其他合并工具,你可以在 “下列工具中(one of the following tools)” 这句后面看到所有支持的合并工具。 然后输入你喜欢的工具名字就可以了。

检索分支信息

git branch 命令不只是可以创建与删除分支。 如果不加任何参数运行它,会得到当前所有分支的一个列表:

1
2
3
4
$ git branch
  iss53
* master
  testing

注意 master 分支前的 * 字符:它代表现在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)。 这意味着如果在这时候提交,master 分支将会随着新的工作向前移动。 如果需要查看每一个分支的最后一次提交,可以运行 git branch -v 命令:

1
2
3
4
$ git branch -v
  iss53   93b412c fix javascript issue
* master  7a98805 Merge branch 'iss53'
  testing 782fd34 add scott to the author list in the readmes

--merged--no-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。 如果要查看哪些分支已经合并到当前分支,可以运行 git branch --merged

1
2
3
$ git branch --merged
  iss53
* master

因为之前已经合并了 iss53 分支,所以现在看到它在列表中。 在这个列表中分支名字前没有 * 号的分支通常可以使用 git branch -d 删除掉;你已经将它们的工作整合到了另一个分支,所以并不会失去任何东西。

查看所有包含未合并工作的分支,可以运行 git branch --no-merged

1
2
$ git branch --no-merged
  testing

这里显示了其他分支。 因为它包含了还未合并的工作,尝试使用 git branch -d 命令删除它时会失败:

1
2
3
$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

如果真的想要删除分支并丢掉那些工作,如同帮助信息里所指出的,可以使用 -D 选项强制删除它。

工作流介绍

lr-branches-2

Figure 7. 趋于稳定分支的流水线(“silo”)视图

一般情况下我们会基于长期稳定的 master 分支拉出用于后续开发和稳定性测试的 develop 分支;我们也会经常基于开发的分支拉出一些类似 bugfix 的主题分支。

变基

整合分支最容易的方法是 merge 命令。 它会把两个分支的最新快照 C3C4 以及二者最近的共同祖先 C2 进行三方合并,合并的结果是生成一个新的快照 C5 并提交。

通过合并操作来整合分叉了的历史。

Figure 8. 通过合并操作来整合分叉的历史

其实,还有一种方法:你可以提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次。 在 Git 中,这种操作就叫做 变基(rebase)。 你可以使用 rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。

在这个例子中,你可以检出 experiment 分支,然后将它变基到 master 分支上:

1
2
3
4
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master) 的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件, 然后将当前分支指向目标基底 C3,最后以此将之前另存为临时文件的修改依序应用。

将 `C4` 中的修改变基到 `C3` 上。

Figure 9. 将 C4 中的修改变基到 C3

现在回到 master 分支,进行一次快进合并:

1
2
$ git checkout master
$ git merge experiment

`master` 分支的快进合并。

Figure 10. master 分支的快进合并

这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。

一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个其他人维护的项目贡献代码时。 在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

Note: 如果你对已经推送过的提交执行变基,但别人没有基于该提交进行开发,那么不会有事。 如果你对已经推送至共用仓库的提交上执行变基命令,并因此丢失了一些他人开发所基于的提交, 那你就有大麻烦了,你的同事也会因此鄙视你。

如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。

变基 vs. 合并

至此,你已在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。 在回答这个问题之前,让我们退后一步,想讨论一下提交历史到底意味着什么。

有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用 谎言 掩盖了实际发生过的事情。 如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。

另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebasefilter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。

现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。

Reference

  • https://git-scm.com/book

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。