git-book 总结

起步

关于版本控制

版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。

下面介绍几种不同类型的版本控制系统。

本地版本控制系统(Local Version Control System, 简称 LVCS)

LVCS, 只存在于本地的版本控制系统,一般使用某种简单数据库来记录文件的历次更新差异,如RCS。
LVCS 最大的缺点就是无法与他人协作。 Local.png

集中化的版本控制系统(Centralized Version Control Systems,简称CVCS)

为解决LVCS的开发者协作问题,出现了CVCS,其最大特点是有一个单一集中的管理服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新,如SVN。
CVCS 最大的缺点是中央服务器的单点故障。 Centralized

分布式版本控制系统(Distributed Version Control System,简称 DVCS)

于是DVCS应运而生。DVCS 客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。 distributed

Git基础

接下来的内容非常重要,若你理解了Git的思想和基本工作原理,用起来就会知其所以然,游刃有余。
Git在保存和对待各种信息的时候与其它版本控制系统有很大差异,理解这些差异将有助于防止你使用中的困惑。

直接记录快照,而非比较差异

Git和其它版本控制系统的主要差别在于Git对待数据的方法。概念上来区分,其它大部分系统以文件变更列表的方式存储信息。这类系统将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。 deltas.png

Git不按照以上方式对待或保存数据。反之,Git更像是把数据看作是对小型文件系统的一组快照。每次你提交更新,或在Git中保存项目状态时,它会对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,Git不再重新存储该文件,而是只保留一个链接指向之前存储的文件。
Git对待数据更像是一个快照流: snapshots.png

这是Git与几乎所有其它版本控制系统的重要区别。

近乎所有操作都是本地执行

由于本地有完整的版本库,使得Git中的绝大多数操作都只需要访问本地文件和资源。这也意味着你离线或者没有VPN时,几乎可以进行任何操作。

Git保证完整性

Git中所有数据在存储前都计算校验和,然后以校验和来引用。这意味着不可能在Git不知情时更改任何文件内容或目录内容。这个功能建构在Git底层,是构成Git哲学不可或缺的部分。若你在传送过程中丢失信息或损坏文件,Git就能发现。
Git用以计算校验和的机制叫做SHA-1散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成字符串,基于 Git 中文件的内容或目录结构计算出来。
Git中使用这种哈希值的情况很多,你将经常看到这种哈希值。 实际上,Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。

Git一般只添加数据

你执行的Git操作,几乎只往Git数据库中增加数据。很难让Git执行任何不可逆操作,或者让它以任何方式清除数据。同别的VCS一样,未提交更新时有可能丢失或弄乱修改的内容;但是一旦你提交快照到Git中,就难以再丢失数据,特别是如果你定期的推送数据库到其它仓库的话。

三种状态

Git有三种状态,你的文件可能处于其中之一:
已提交(committed): 表示数据已经安全的保存在本地数据库中。
已修改(modified): 表示修改了文件,还没有保存到数据库中。
已暂存(staged): 表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。
由此引入Git项目的三个工作区域的概念:Git仓库、工作目录以及暂存区域 areas.png Git仓库目录是Git用来保存项目的元数据和对象数据库的地方。 这是Git中最重要的部分,从其它计算机克隆仓库时,拷贝的就是这里的数据。
工作目录是对项目的某个版本独立提取出来的内容。这些从Git仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。
暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在Git仓库目录中。有时候也被称作索引,不过一般说法还是叫暂存区域。

基本的 Git 工作流程如下:

  1. 在工作目录中修改文件。
  2. 暂存文件,将文件的快照放入暂存区域。
  3. 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。

初次运行Git前的配置

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

  1. /etc/gitconfig: 包含系统上每一个用户及他们仓库的通用配置。如果使用带有 --system 选项的 git config 时,它会从此文件读写配置变量。
  2. ~/.gitconfig~/.config/git/config 文件: 只针对当前用户。 可以传递 --global 选项让Git读写此文件。
  3. 仓库的Git目录中的config文件(就是 .git/config ): 针对该仓库。

每一个级别覆盖上一级别的配置,所以 .git/config 的配置变量会覆盖 /etc/gitconfig 中的配置变量。
在Windows系统中,Git会查找$HOME目录下(一般情况下是 C:\Users\$USER)的 .gitconfig 文件。 Git 同样也会寻找 /etc/gitconfig 文件,但只限于 MSys的根目录下,即安装Git时所选的目标位置。

用户信息

当安装完 Git 应该做的第一件事就是设置你的用户名称与邮件地址。 这样做很重要,因为每一个 Git 的提交都会使用这些信息,并且它会写入到你的每一次提交中,不可更改:

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

再次强调,如果使用了 –global 选项,那么该命令只需要运行一次,因为之后无论你在该系统上做任何事情, Git 都会使用那些信息。 当你想针对特定项目使用不同的用户名称与邮件地址时,可以在那个项目目录下运行没有 –global 选项的命令来配置。

文本编辑器

当 Git 需要你输入信息时会调用文本编辑器。 如果未配置,Git 会使用操作系统默认的文本编辑器,通常是 Vim。 如果你想使用不同的文本编辑器,例如 Emacs,可以这样做:

$ git config --global core.editor emacs

检查配置信息

如果想要检查你的配置,可以使用 git config --list 命令来列出所有 Git 当时能找到的配置。

$ git config --list
user.name=John Doe
user.email=johndoe@example.com
color.status=auto
color.branch=auto
color.interactive=auto
color.diff=auto
...

你可能会看到重复的变量名,因为 Git 会从不同的文件中读取同一个配置(例如:/etc/gitconfig 与 ~/.gitconfig)。 这种情况下,Git 会使用它找到的每一个变量的最后一个配置。
你可以通过输入 git config : 来检查 Git 的某一项配置

$ git config user.name
John Doe

获取帮助

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

$ git help <verb>
$ git <verb> --help
$ man git-<verb>

例如,要想获得 config 命令的手册,执行

$ git help config

Git基础

获取Git仓库

有两种取得 Git 项目仓库的方法。 第一种是在现有项目或目录下导入所有文件到 Git 中; 第二种是从一个服务器克隆一个现有的 Git 仓库。

在现有目录中初始化仓库

$ git init

该命令创建一个名为 .git 的子目录,这个子目录含有你初始化的 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干.

克隆现有的仓库

Git 克隆的是该 Git 仓库服务器上的几乎所有数据,而不仅仅复制完成你的工作所需要文件。当你执行 git clone 命令的时候,默认配置下远程 Git 仓库中的每一个文件的每一个版本都将被拉取下来。
克隆仓库的命令格式是 git clone [url]。 比如:

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

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

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

Git 支持多种数据传输协议,包括https://协议,git:// 协议或者SSH传输协议,比如 user@server:path/to/repo.git

记录每次更新到仓库

文件的状态变化周期

工作目录下的每一个文件都不外乎这两种状态:已跟踪或未跟踪。
已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录。
工作目录中除已跟踪文件以外的所有其它文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有放入暂存区。
文件的状态变化周期: lifecycle.png

检查当前文件状态

要查看哪些文件处于什么状态,可以用 git status 命令。 阅读命令输出查看具体信息。

$ git status

跟踪新文件

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

$ git add README

git add 命令使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件

暂存已修改文件

要暂存文件更新,运行 git add 命令。 注意Git 只暂存了运行 git add 命令时的版本。
git add是个多功能命令:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态等。

状态简览

git status 命令的输出十分详细,但其用语有些繁琐。 如果你使用 git status -s 命令或 git status –short 命令,你将得到一种更为紧凑的格式输出。 运行 git status -s ,状态报告输出如下:

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

新添加的未跟踪文件前面有 ?? 标记,新添加到暂存区中的文件前面有 A 标记,修改过的文件前面有 M 标记。 你可能注意到了 M 有两个可以出现的位置,出现在右边的 M 表示该文件被修改了但是还没放入暂存区,出现在靠左边的 M 表示该文件被修改了并放入了暂存区。 例如,上面的状态报告显示: README 文件在工作区被修改了但是还没有将修改后的文件放入暂存区,lib/simplegit.rb 文件被修改了并将修改后的文件放入了暂存区。 而 Rakefile 在工作区被修改并提交到暂存区后又在工作区中被修改了,所以在暂存区和工作区都有该文件被修改了的记录。

忽略文件

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

$ cat .gitignore
*.[oa]
*~

第一行告诉 Git 忽略所有以 .o 或 .a 结尾的文件。一般这类对象文件和存档文件都是编译过程中出现的。 第二行告诉 Git 忽略所有以波浪符(~)结尾的文件,许多文本编辑软件(比如 Emacs)都用这样的文件名保存副本。 此外,你可能还需要忽略 log,tmp 或者 pid 目录,以及自动生成的文档等等。 要养成一开始就设置好 .gitignore 文件的习惯,以免将来误提交这类无用的文件。

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

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

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

我们再看一个 .gitignore 文件的例子:

# no .a files
*.a

# but do track lib.a, even though you're ignoring .a files above
!lib.a

# only ignore the TODO file in the current directory, not subdir/TODO
/TODO

# ignore all files in the build/ directory
build/

# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt

# ignore all .pdf files in the doc/ directory
doc/**/*.pdf

查看已暂存和未暂存的修改

git diff 通过文件补丁的格式显示具体哪些行发生了改变。
git diff : 比较工作目录中当前文件和暂存区域快照之间的差异, 也就是修改之后还没有暂存起来的变化内容。
git diff --cached : 查看已暂存的将要添加到下次提交里的内容(或者git diff --staged)。
git diff HEAD : 比较工作目录与本地仓库中最新提交的差异。(git diffgit diff --cached 的差异和)

提交更新

git commit : 提交已暂存的更新 (记录放在暂存区域的快照)。
这种方式会启动设置的或者默认的文本编辑器以便输入本次提交的说明。默认的提交消息包含最后一次运行 git status的输出,放在注释行里,另外开头还有一空行,供你输入提交说明。 可以用 -v 选项,将在编辑器中显示git diff --cached 的输出。 退出编辑器时,Git 会丢掉注释行,用你输入提交附带信息生成一次提交。(如果没有输入提交信息,将放弃本次提交)

另外,你也可以在 commit 命令后添加 -m 选项,将提交信息与命令放在同一行,如下所示:

$ 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

提交后它会告诉你,当前是在哪个分支(master)提交的,本次提交的完整 SHA-1 校验和是什么(463dc4f),以及在本次提交中,有多少文件修订过,多少行添加和删改过。

跳过使用暂存区域

git commit 加上 -a 选项, 自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤。

移除文件

git rm: 从Git中移除文件,即从已跟踪文件清单中移除(确切地说,是从暂存区域移除),并连带从工作目录中删除指定的文件。
如果只从工作目录中手工删除文件(如rm <file>),运行 git status 时就会在 “Changes not staged for commit” 部分(也就是未暂存清单)看到,此时根据提示还需要运行 git add/rm <fill>... 暂存此次移除文件的操作。
如果文件删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f ,这是一种安全特性,用于防止误删还没有添加到快照的数据,这样的数据不能被 Git 恢复。
git rm --cached: 把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然保留在当前工作目录中,此时文件是未跟踪状态。
git rm 命令后面可以列出文件或者目录的名字,也可以使用 glob 模式。 比方说:

$ git rm log/\*.log
$ git rm \*~

移动文件

不像其它的 VCS 系统,Git 并不显式跟踪文件移动操作。 如果在 Git 中重命名了某个文件,仓库中存储的元数据并不会体现出这是一次改名操作。 不过 Git 非常聪明,它会推断出究竟发生了什么。
要在 Git 中对文件改名,可以这么做:
$ git mv file_from file_to
它会恰如预期般正常工作。 实际上,即便此时查看状态信息,也会明白无误地看到关于重命名操作的说明:

$ git mv README.md README
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

其实,运行 git mv 就相当于运行了下面三条命令:

$ mv README.md README
$ git rm README.md
$ git add README

如此分开操作,Git 也会意识到这是一次改名,所以不管何种方式结果都一样。 两者唯一的区别是,mv 是一条命令而另一种方式需要三条命令,直接用 git mv 轻便得多。 不过有时候用其他工具批处理改名的话,要记得在提交前删除老的文件名,再添加新的文件名。

查看提交历史

不加参数git log

git log 默认不用任何参数的话,会按提交时间列出所有的更新,最近的更新排在最上面。这个命令会列出每个提交的SHA-1校验和、作者的名字和电子邮件地址、提交时间以及提交说明。
git log 有许多选项可以帮助你搜寻你所要找的提交, 接下来我们介绍些最常用的。

-p 选项

一个常用的选项是 -p,用来显示每次提交的内容差异。 你也可以加上 -2 来仅显示最近两次提交:

$ 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
\ No newline at end of file

--stat 选项

--stat 选项显示每次提交的简略的统计信息。
它会在每次提交的下面列出所有被修改过的文件、有多少文件被修改了以及被修改过的文件的哪些行被移除或是添加了。 在每次提交的最后还有一个总结。如:

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

    changed the version number

 Rakefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

    removed unnecessary test

 lib/simplegit.rb | 5 -----
 1 file changed, 5 deletions(-)

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

    first commit

 README           |  6 ++++++
 Rakefile         | 23 +++++++++++++++++++++++
 lib/simplegit.rb | 25 +++++++++++++++++++++++++
 3 files changed, 54 insertions(+)

--pretty 选项

另外一个常用的选项是 --pretty。 这个选项可以指定使用不同于默认格式的方式展示提交历史。 这个选项有一些内建的子选项供你使用。 比如用 oneline 将每个提交放在一行显示,查看的提交数很大时非常有用。 另外还有 shortfullfuller可以用,展示的信息或多或少有些不同,请自己动手实践一下看看效果如何。

$ git log --pretty=oneline
ca82a6dff817ec66f44342007202690a93763949 changed the version number
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test
a11bef06a3f659402fe7563abf99ad00de2209e6 first commit

但最有意思的是 format,可以定制要显示的记录格式。 这样的输出对后期提取分析格外有用,因为你知道输出的格式不会随着 Git 的更新而发生改变:

$ 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
git log --pretty=format 常用的选项 列出了常用的格式占位符写法及其代表的意义。

Table 1. git log --pretty=format 常用的选项

选项 说明
%H 提交对象(commit)的完整哈希字串
%h 提交对象的简短哈希字串
%T 树对象(tree)的完整哈希字串
%t 树对象的简短哈希字串
%P 父对象(parent)的完整哈希字串
%p 父对象的简短哈希字串
%an 作者(author)的名字
%ae 作者的电子邮件地址
%ad 作者修订日期(可以用 –date= 选项定制格式)
%ar 作者修订日期,按多久以前的方式显示
%cn 提交者(committer)的名字
%ce 提交者的电子邮件地址
%cd 提交日期
%cr 提交日期,按多久以前的方式显示
%s 提交说明

实作者指的是实际作出修改的人,提交者指的是最后将此工作成果提交到仓库的人。 在分布式 Git 再详细介绍两者之间的细微差别。
onelineformat 与另一个 log 选项 --graph 结合使用时尤其有用。 这个选项添加了一些ASCII字符串来形象地展示你的分支、合并历史:

$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 ignore errors from SIGCHLD on trap
*  5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Added a method for getting the current branch.
* | 30e367c timeout code and tests
* | 5a09431 add timeout protection to grit
* | e1193f8 support for heads with slashes in them
|/
* d6016bc require time for xmlschema
*  11d191e Merge branch 'defunkt' into local

Table 2. git log 的常用选项

选项 说明
-p 按补丁格式显示每个更新之间的差异。
–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(后跟指定格式)。

限制输出长度

git log 还有许多非常实用的限制输出长度的选项,也就是只输出部分提交信息。 如:
-<n> 选项,n 可以是任何整数,表示仅显示最近的若干条提交。
按照时间作限制的选项,比如 --since--until。 例如:
下面的命令列出所有最近两周内的提交:
$ git log --since=2.weeks
这个命令可以在多种格式下工作,比如说具体的某一天 “2008-01-15”,或者是相对地多久以前 “2 years 1 day 3 minutes ago”。 设置搜索条件,列出符合的提交。 如用 --author 选项显示指定作者的提交, --grep 选项搜索提交说明中的关键字。 (请注意,如果要得到同时满足这两个选项搜索条件的提交,就必须用 --all-match 选项。否则,满足任意一个条件的提交都会被匹配出来)
另一个非常有用的筛选选项是 -S,可以列出那些添加或移除了某些字符串的提交。 比如说,你想找出添加或移除了某一个特定函数的引用的提交,你可以这样使用:
$ git log -Sfunction_name
最后一个很实用的 git log 选项是路径(path), 如果只关心某些文件或者目录的历史提交,可以在 git log 选项的最后指定它们的路径。 因为是放在最后位置上的选项,所以用两个短划线(--)隔开之前的选项和后面限定的路径名。

Table 3. 限制 git log 输出的选项

选项 说明
-(n) 仅显示最近的 n 条提交
–since, –after 仅显示指定时间之后的提交。
–until, –before 仅显示指定时间之前的提交。
–author 仅显示指定作者相关的提交。
–committer 仅显示指定提交者相关的提交。
–grep 仅显示含指定关键字的提交
-S 仅显示添加或移除了某个关键字的提交

来看一个实际的例子,如果要查看 Git 仓库中,2008 年 10 月期间,Junio Hamano 提交的但未合并的测试文件,可以用下面的查询命令:

$ git log --pretty="%h - %s" --author=gitster --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

撤消操作

--amend 重新提交选项

$ git commit --amend 这个命令会将暂存区中的文件提交, 并且覆盖上一次的提交信息。 举例:

$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend

取消暂存的文件

git reset HEAD <file>... 取消暂存的文件。

撤消对文件的修改

git checkout -- <file>... 撤销对文件的修改

Important
你需要知道 git checkout -- [file] 是一个危险的命令,这很重要。 你对那个文件做的任何修改都会消失 - 你只是拷贝了另一个文件来覆盖它。 除非你确实清楚不想要那个文件了,否则不要使用这个命令。

记住,在 Git 中任何已提交的东西几乎总是可以恢复的。 甚至那些被删除的分支中的提交或使用 --amend 选项覆盖的提交也可以恢复(阅读数据恢复了解数据恢复)。 然而,任何你未提交的东西丢失后很可能再也找不到了。

远程仓库的使用

远程仓库是指托管在因特网或其他网络中的你的项目的版本库。 你可以有好几个远程仓库,通常有些仓库对你只读,有些则可以读写。 与他人协作涉及管理远程仓库以及根据需要推送或拉取数据。 管理远程仓库包括了解如何添加远程仓库、移除无效的远程仓库、管理不同的远程分支并定义它们是否被跟踪等等。

查看远程仓库

git remote 列出已经配置的所有远程仓库服务器的简写。 克隆的仓库服务器的默认名字是origin。
-v 选项 ,显示远程仓库的简写与其对应的 URL。

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

如果你的远程仓库不止一个,该命令会将它们全部列出。 例如,与几个协作者合作的,拥有多个远程仓库的仓库看起来像下面这样:

$ cd grit
$ git remote -v
bakkdoor  https://github.com/bakkdoor/grit (fetch)
bakkdoor  https://github.com/bakkdoor/grit (push)
cho45     https://github.com/cho45/grit (fetch)
cho45     https://github.com/cho45/grit (push)
defunkt   https://github.com/defunkt/grit (fetch)
defunkt   https://github.com/defunkt/grit (push)
koke      git://github.com/koke/grit.git (fetch)
koke      git://github.com/koke/grit.git (push)
origin    git@github.com:mojombo/grit.git (fetch)
origin    git@github.com:mojombo/grit.git (push)

这样我们可以轻松拉取其中任何一个用户的贡献。 此外,我们大概还会有某些远程仓库的推送权限。

git remote show [remote-name] 查看某一个远程仓库的更多信息,如:

$ git remote show origin
* remote origin
  URL: https://github.com/my-org/complex-project
  Fetch URL: https://github.com/my-org/complex-project
  Push  URL: https://github.com/my-org/complex-project
  HEAD branch: master
  Remote branches:
    master                           tracked
    dev-branch                       tracked
    markdown-strip                   tracked
    issue-43                         new (next fetch will store in remotes/origin)
    issue-45                         new (next fetch will store in remotes/origin)
    refs/remotes/origin/issue-11     stale (use 'git remote prune' to remove)
  Local branches configured for 'git pull':
    dev-branch merges with remote dev-branch
    master     merges with remote master
  Local refs configured for 'git push':
    dev-branch                     pushes to dev-branch                     (up to date)
    markdown-strip                 pushes to markdown-strip                 (up to date)
    master                         pushes to master                         (up to date)

它会列出远程仓库的URL与跟踪分支的信息, 当前正处于的分支。
当你在特定的分支上执行 git push 时会自动地推送到哪一个远程分支。 列出了哪些远程分支不在你的本地,哪些远程分支已经从服务器上移除了,还有当你执行 git pull 时哪些分支会自动合并。

添加远程仓库

git remote add <shortname> <url> 添加一个新的远程 Git 仓库,同时指定一个你可以轻松引用的简写:

$ git remote
origin
$ 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)

现在你可以在命令行中使用字符串 pb 来代替整个 URL。 例如,如果你想拉取 Paul 的仓库中有但你没有的信息,可以运行 git fetch pb

$ git fetch pb
remote: Counting objects: 43, done.
remote: Compressing objects: 100% (36/36), done.
remote: Total 43 (delta 10), reused 31 (delta 5)
Unpacking objects: 100% (43/43), done.
From https://github.com/paulboone/ticgit
 * [new branch]      master     -> pb/master
 * [new branch]      ticgit     -> pb/ticgit

现在 Paul 的 master 分支可以在本地通过 pb/master 访问到,你可以将它合并到自己的某个分支中,或者如果你想要查看它的话,可以检出一个指向该点的本地分支。

从远程仓库中抓取与拉取

$ git fetch [remote-name]
这个命令会访问远程仓库,从中拉取所有你还没有的数据。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。
注意 git fetch 命令会将数据拉取到你的本地仓库,它并不会自动合并或修改你当前的工作。 当准备好时再手动将其合并入你的工作。
如果你有一个分支设置为跟踪一个远程分支,可以使用 git pull 命令来自动的抓取然后合并远程分支到当前分支。

推送到远程仓库

git push [remote-name] [branch-name]
运行这条命令需要有远程仓库的写入权限,并且远程分支的提交不比本地快。 如果远程分支比本地更快(其他人的提交),本次推送将被拒绝,必须先将远程分支拉取下来并将其合并进你的工作后才能推送。

远程仓库的移除与重命名

git remote rename 修改一个远程仓库的简写名。 例如:

$ git remote rename pb paul
$ git remote
origin
paul

值得注意的是这同样也会修改你的远程分支名字。 那些过去引用 pb/master 的现在会引用 paul/master。

git remote rm [remote-name] 移除一个远程仓库。

$ git remote rm paul
$ git remote
origin

打标签

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

列出标签

git tag: 以字母顺序列出标签。
-l 选项使用特定的模式查找标签, 如:

$ git tag -l 'v1.8.5*'
v1.8.5
v1.8.5-rc0
v1.8.5-rc1

创建标签

Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。
一个轻量标签很像一个不会改变的分支 - 它只是一个特定提交的引用。

然而,附注标签是存储在 Git 数据库中的一个完整对象。 它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用 GNU Privacy Guard (GPG)签名与验证。 通常建议创建附注标签,这样你可以拥有以上所有信息;但是如果你只是想用一个临时的标签,或者因为某些原因不想要保存那些信息,轻量标签也是可用的。

附注标签

git tag -a [tag-name] 创建附注标签:

$ git tag -a v1.4 -m 'my version 1.4'

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

git show [tag-name] 查看标签信息与对应的提交信息:

$ 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

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

轻量标签

git tag [tag-name] 创建轻量标签,本质上是将 提交校验和 存储到一个文件中,没有保存任何其他信息。

$ git tag v1.4-lw
$ 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

git show [tag-name] 只显示提交信息。

后期打标签

git tag [-a] [tag-name] [校验和] 在命令的末尾指定提交的校验和。

共享标签

默认情况下,git push 命令并不会传送标签到远程仓库服务器上。 在创建完标签后你必须显式地推送标签到共享服务器上。

git push [remote-name] [tagname] 推送标签到远程仓库
git push [remote-name] --tags 推送所有不在远程仓库服务器上的标签全部

检出标签

在 Git 中你并不能真的检出一个标签,因为它们并不能像分支一样来回移动。 如果你想要工作目录与仓库中特定的标签版本完全一样, 可以使用 git checkout -b [branchname] [tagname] 在特定的标签上创建一个新分支:

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

Git 别名

别名使你的 Git 体验更简单、容易、熟悉。
Git 并不会在你输入部分命令时自动推断出你想要的命令。 如果不想每次都输入完整的 Git 命令,可以通过 git config 文件来轻松地为每一个命令设置一个别名。 这里有一些例子你可以试试:(有点像字符串替换)

$ 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 config --global alias.unstage 'reset HEAD --'  //取消暂存文件
$ git config --global alias.last 'log -1 HEAD'  //查看最后一次提交

Git 只是简单地将别名替换为对应的命令。 如果要替换的是一个 外部命令,而非Git子命令, 那么在命令前面加入 ! 符号。
如将 git visual 定义为 gitk 的别名:
$ git config --global alias.visual '!gitk'

Git 分支

分支简介

内部原理

使用分支可以把你的工作从开发主线上分离开来,以免影响开发主线。Git对分支的管理和操作非常的高效便捷,这得益于它将数据看作是一个文件系统快照流的处理方式。
git add暂存操作会为 每一个文件 计算校验和,然后将当前版本的文件快照保存到 Git 仓库中(Git使用blob对象来保存它们),最终将校验和加入到暂存区域等待提交。
git commit提交操作会先计算每一个 子目录 的校验和,然后在 Git 仓库中这些校验和保存为 树对象 。 随后,Git 便会创建一个 提交对象 ,它除了包含作者的姓名和邮箱、提交时输入的信息以及指向它的 父对象 (上一个提交对象,可能有一个,多个,或者没有)的指针,还包含指向这个树对象(项目根目录)的指针。这样,Git就可以在需要的时候重现此次保存的快照。
下面是一个包含三个将要被暂存和提交的文件的根目录的例子:

git add REDAME test.rb LICENSE
git commit - m "The initial commit of my project"

现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息) 如下图: commit-and-tree.png (注意提交对象包含根目录tree(树对象)的校验和索引)

对于有子目录的情况(同样的,tree树对象包含lib子目录(树对象)的校验和索引),如下图: data-model-1.png

提交对象及其父对象: commits-and-parents.png

Git 的分支,其本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。
分支及其提交历史(HEAD指向当前分支,可以理解为当前分支的别名): branch-and-history.png

分支创建

git branch [branch-name] 在当前所在提交对象上创建一个可以移动的指针(不会切换到新的分支)。如:

$ git branch testing

head-to-master.png

使用 git log 命令查看各个分支当前所指的提交对象。 提供这一功能的参数是 --decorate

$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit of my project

分支切换

git checkout [branch-name] 切换到 branch-name 分支
$ git checkout testing
head-to-testing.png

再提交一次:

$ vim test.rb
$ git commit -a -m 'made a change'

HEAD 分支随着提交操作自动向前移动: advance-testing

如图所示,testing 分支向前移动了,但是 master 分支却没有,它仍然指向运行 git checkout 时所指的对象。
现在切换回 master 分支看看:

$ git checkout master

检出时 HEAD 随之移动: checkout-master

这条命令做了两件事。 一是使 HEAD 指回 master 分支,二是将工作目录恢复成 master 分支所指向的快照内容。

分支切换会改变你工作目录中的文件
在切换分支时,一定要注意你工作目录里的文件会被改变。 如果是切换到一个较旧的分支,你的工作目录会恢复到该分支最后一次提交时的样子。 如果 Git 不能干净利落地完成这个任务,它将禁止切换分支。(要留意你的工作目录和暂存区里那些还没有被提交的修改,它可能会和你即将检出的分支产生冲突从而阻止 Git 切换到该分支)

在master分支修改并提交:

$ vim test.rb
$ git commit -a -m 'made other changes'

项目分叉历史: advance-master.png

可以在不同分支间不断地来回切换和工作,并在时机成熟时将它们合并起来。
使用 git log 命令查看分叉历史。 运行 git log --oneline --decorate --graph --all ,它会输出你的提交历史、各个分支的指向以及项目的分支分叉情况。

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project

由于 Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。 创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符)。
在 Git 中,任何规模的项目都能在瞬间创建新分支。 同时,由于每次提交都会记录父对象,所以寻找恰当的合并基础(译注:即共同祖先)也是同样的简单和高效。 这些高效的特性使得 Git 鼓励开发人员频繁地创建和使用分支。

分支的新建与合并

新建分支

git checkout -b [branch-name] 新建一个分支并切换到新分支上。 相当于如下两条命令:

git branch [branch-name]
git checkout [branch-name]

分支合并

快进(fast-forward)合并

基于 master 分支的紧急问题分支 hotfix branch: basic-branching-4.png

如图所示,git merge 将hotfix分支合并到master分支

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

由于当前 master 分支所指向的提交是你当前提交(有关 hotfix 的提交)的 直接上游 ,所以 Git 只是简单的将指针向前移动。 这种情况下的合并操作没有需要解决的分歧,所以叫做 “快进(fast-forward)合并”。 basic-branching-5.png

删除已合并分支

git branch -d [branch-name] 删除已合并分支

三方合并

假设完成了iss53分支的工作,合并到master分支:

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

开发历史从一个更早的地方开始分叉开来(diverged),master 分支所在提交并不是 iss53 分支所在提交的直接祖先。 此时,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及自行选择的两个分支的工作祖先(C2),做一个简单的三方合并,如下图所示: basic-merging-1.png

Git 将此次三方合并的结果作为一个新的快照提交(合并提交),他有不止一个父提交。 basic-merging-2.png

解决合并冲突

如果在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,合并会产生冲突。
此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。
使用 git status 命令查看那些因包含合并冲突而处于未合并(unmerged)状态的文件。
Git会在有冲突的文件中加入标准的 冲突解决标记 ,打开这些包含冲突的文件然后手动解决冲突。
出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:

<<<<<<< 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分支版本在 ======= 的上半部分, iss53 分支版本在 ======= 的下半部分。
可以选择由 =======分割的两部分中的一个,或自行合并这些内容。
例如:

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

然后使用 git add 暂存所有冲突文件将其标记为冲突已解决。
解决所有冲突并暂存后,运行 git commit 来完成合并提交。

分支管理

列出当前所有分支

git branch 显示当前所有分支:

查看所有分支的最后一次提交

git branch -v 查看每一个分支的最后一次提交。

查看已合并或未合并到当前分支的分支

git branch --merged
git branch --no-merged
可以用 git branch -d 将已合并到当前分支的分支删除,而不会丢失任何信息。
使用 git branch -d 命令删除未合并的分支会失败。
可以使用 git branch -D 命令强制删除并丢弃未合并分支所有工作。

分支开发工作流

长期分支

在整个项目开发周期的不同阶段,可以同时拥有多个开放的分支;定期地把某些特性分支合并入其他分支中。
比如只在 master 分支上保留完全稳定的代码——有可能仅仅是已经发布或即将发布的代码。 还有一些名为 develop 或者 next 的平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master 分支了。 这样,在确保这些已完成的特性分支能够通过所有测试,并且不会引入更多 bug 之后,就可以合并入主干分支中,等待下一次的发布。 渐进稳定分支的线性图: lr-branches-1.png

把他们想象成流水线(work silos)更好理解一点,那些经过测试考验的提交会被遴选到更加稳定的流水线上去: lr-branches-2.png

用这种方法可以维护不同层次的稳定性。 当一个分支具有一定程度的稳定性后,把它们合并入具有更高级别稳定性的分支中。

特性分支

特性分支对任何规模的项目都适用。 特性分支是一种短期分支,它被用来实现单一特性或其相关工作。
考虑这样一个例子,你在 master 分支上工作到 C1,这时为了解决一个问题而新建 iss91 分支,在 iss91 分支上工作到 C4,然而对于那个问题你又有了新的想法,于是你再新建一个 iss91v2 分支试图用另一种方法解决那个问题,接着你回到 master 分支工作了一会儿,你又冒出了一个不太确定的想法,你便在 C10 的时候新建一个 dumbidea 分支,并在上面做些实验。 你的提交历史看起来像下面这个样子: topic-branches-1.png

现在,我们假设两件事情:你决定使用第二个方案来解决那个问题,即使用在 iss91v2 分支中方案;另外,你将 dumbidea 分支拿给你的同事看过之后,结果发现这是个惊人之举。 这时你可以抛弃 iss91 分支(即丢弃 C5 和 C6 提交),然后把另外两个分支合并入主干分支: topic-branches-2.png

远程分支

远程跟踪分支

(remote)/(branch) 形式命名的 远程跟踪分支 是远程分支状态的引用。它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。 远程跟踪分支像是你上次连接到远程仓库时,那些分支所处状态的书签。
举个例子,假设你从Git服务器 git.ourcompany.com 克隆,Git自动将其命名为 origin ,拉取它的所有数据,创建一个本地 origin/master 指针指向远程的 master 分支。Git同时会创建一个与 origin/master 分支指向同一个地方的本地 master 分支,这样你就有工作的基础。
克隆之后的服务器与本地仓库: remote-branches-1.png

同步远程分支

git fetch originorigin 中抓取本地没有的数据,并且更新本地数据库,移动 origin/master 指针指向新的、更新后的位置。 git fetch 更新远程仓库引用: remote-branches-3.png

多个远程仓库与远程分支

使用git remote add teamone [url] 命令添加一个新的远程仓teamone库引用到当前的项目。
使用git fetch teamone 抓取远程仓库teamone有而本地没有的数据。
因为 teamone 服务器上现有的数据是 origin 服务器上的一个子集,所以 Git 并不会抓取数据而是会设置远程跟踪分支 teamone/master 指向 teamonemaster 分支。 remote-branches-5.png

推送

本地的分支并不会自动与远程仓库同步,必须显式地推送想要分享的分支。语法:
git push (remote) (branch)
例如 git push origin serverfix 命令推送本地的 serverfix 分支来更新远程仓库上的 serverfix分支, 因为Git 自动将 serverfix 分支名字展开为 refs/heads/serverfix:refs/heads/serverfix。这等价于 git push origin serverfix:serverfix, 可以通过这种格式来推送本地分支到一个命名不相同的远程分支,如git push origin serverfix:awesomebranch
下一次其他协作者从服务器上抓取数据时,他们会在本地生成一个远程分支 origin/serverfix,指向服务器的 serverfix 分支的引用:

$ git fetch origin
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/schacon/simplegit
 * [new branch]      serverfix    -> origin/serverfix

要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。 换一句话说,这种情况下,不会有一个新的 serverfix 分支 - 只有一个不可以修改的 origin/serverfix 指针。

可以运行 git merge origin/serverfix 将这些工作合并到当前所在的分支。 如果想要在自己的 serverfix 分支上工作,可以将其建立在远程跟踪分支之上:

$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

这会给你一个用于工作的本地分支,并且起点位于 origin/serverfix。

创建或设置跟踪分支

从一个远程跟踪分支( origin/master )检出一个本地分支会自动创建一个叫做 “跟踪分支”( master )(有时候也叫做 “上游分支”)。 跟踪分支是与远程分支有直接关系的本地分支。 如果在一个跟踪分支上输入 git pull,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。
创建一个跟踪分支:
git checkout -b [branch] [remotename]/[branch] (branch可以不同名)。
git checkout --track [remotename]/[branch] (branch 同名)

设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,你可以在任意时间使用 -u 或 –set-upstream-to 选项运行 git branch 来显式地设置。

$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.

上游快捷方式
当设置好跟踪分支后,可以通过 @{upstream} 或 @{u} 快捷方式来引用它。 所以在 master 分支时并且它正在跟踪 origin/master 时,如果愿意的话可以使用 git merge @{u} 来取代 git merge origin/master。

查看跟踪分支

如果想要查看设置的所有跟踪分支,可以使用 git branch -vv 选项。 这会将所有的本地分支列出来并且包含更多的信息,如每一个分支正在跟踪哪个远程分支与本地分支是否是领先、落后或是都有。

$ git branch -vv
  iss53     7e424c3 [origin/iss53: ahead 2] forgot the brackets
  master    1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
  testing   5ea463a trying something new

这里可以看到 iss53 分支正在跟踪 origin/iss53 并且 “ahead” 是 2,意味着本地有两个提交还没有推送到服务器上。 也能看到 master 分支正在跟踪 origin/master 分支并且是最新的。 接下来可以看到 serverfix 分支正在跟踪 teamone 服务器上的 server-fix-good 分支并且领先 3 落后 1,意味着服务器上有一次提交还没有合并入同时本地有三次提交还没有推送。 最后看到 testing 分支并没有跟踪任何远程分支。

需要重点注意的一点是这些数字的值来自于你从每个服务器上最后一次抓取的数据。 这个命令并没有连接服务器,它只会告诉你关于本地缓存的服务器数据。 如果想要统计最新的领先与落后数字,需要在运行此命令前抓取所有的远程仓库。 可以像这样做:

$ git fetch --all
$ git branch -vv

拉取

git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并。 然而,有一个命令叫作 git pull在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。 如果有一个像之前章节中演示的设置好的跟踪分支,不管它是显式地设置还是通过 clone 或 checkout 命令为你创建的,git pull 都会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据然后尝试合并入那个远程分支。

由于 git pull 的魔法经常令人困惑所以通常单独显式地使用 fetch 与 merge 命令会更好一些。

删除远程分支

假设你已经通过远程分支做完所有的工作了 - 也就是说你和你的协作者已经完成了一个特性并且将其合并到了远程仓库的 master 分支(或任何其他稳定代码分支)。 可以运行带有 –delete 选项的 git push 命令来删除一个远程分支。 如果想要从服务器上删除 serverfix 分支,运行下面的命令:

$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
 - [deleted]         serverfix

基本上这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。

变基(rebase)

在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase(merge参考前文)。

变基的基本操作

举个例子,分支的提交历史如下图: basic-rebase-1.png 提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次, 就叫变基
使用 rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。

$ 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, 最后以此将之前另存为临时文件的修改依序应用。 basic-rebase-3.png

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

$ git checkout master
$ git merge experiment

basic-rebase-4.png

merge 和 rebase 方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。尽管实际的开发工作是并行的,但提交历史是一条直线没有分叉。 首先在自己的分支里进行开发,当开发完成时先将代码变基到 origin/master 上,然后再向主项目推送。

更有趣的变基例子

在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。例如:
从一个特性分支里再分出一个特性分支的提交历史: interesting-rebase-1.pngclient 中的修改合并到 master,但不合并 server 中的修改。
使用 git rebase 命令的 --onto 选项,选中在 client 分支里但不在 server 分支里的修改(即 C8 和 C9),将它们在 master 分支上重放:

$ git rebase --onto master server client

以上命令的意思是:“取出 client 分支,找出处于 client 分支和 server 分支的共同祖先之后的修改,然后把它们在 master 分支上重放一遍”。
截取特性分支上的另一个特性分支,然后变基到其他分支:
interesting-rebase-2.png

快速合并master:

$ git checkout master
$ git merge client

interesting-rebase-3.png

使用 git rebase [basebranch] [topicbranch] 命令可以直接将特性分支(即本例中的 server)变基到目标分支(即 master)上。这样做能省去你先切换到 server 分支,再对其执行变基命令的多个步骤。

$ git rebase master server

interesting-rebase-4.png

快进合并主分支,删除client、server分支

$ git checkout master
$ git merge server
$ git branch -d client
$ git branch -d server

最终提交历史: interesting-rebase-5.png

变基的风险

不要对在你的仓库外有副本的分支执行变基。

变基的实质是丢弃一些现有的提交,然后相应地新建一些 内容一样 但实际上 不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。

看一个在公开的仓库上执行变基操作所带来的问题。
从一个服务器克隆然后在它的基础上进行了一些开发。 提交历史如图所示: perils-of-rebasing-1.png

然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。 你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交历史就会变成这样 perils-of-rebasing-2.png

接下来,这个人又决定把合并操作回滚,改用变基;继而又用 git push --force 命令覆盖了服务器上的提交历史。 之后你从服务器抓取更新,会发现多出来一些新的提交。 perils-of-rebasing-3.png

结果就是你们两人的处境都十分尴尬。 如果你执行 git pull 命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库会如图所示: perils-of-rebasing-4.png

此时执行 git log 命令,会有两个提交的作者、日期、日志居然是一样的,这会令人感到混乱。 此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。 很明显对方并不想在提交历史中看到 C4 和 C6,因为之前就是他把这两个提交通过变基丢弃的。

用变基解决变基

Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和 —— 即 “patch-id”。

如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。 如果你拉取被覆盖过的更新并将你手头的工作基于此进行变基的话,一般情况下 Git 都能成功分辨出哪些是你的修改,并把它们应用到新分支上。 如果有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交,此时执行 git rebase teamone/master 变基而不是合并, Git 将会:

  • 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7)
  • 检查其中哪些提交不是合并操作的结果(C2,C3,C4)
  • 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4’)
  • 把查到的这些提交应用在 teamone/master 上面

在一个被变基然后强制推送的分支上再次执行变基的结果: perils-of-rebasing-5.png

要想上述方案有效,还需要对方在变基时确保 C4’ 和 C4 是几乎一样的。 否则变基操作将无法识别,并新建另一个类似 C4 的补丁(而这个补丁很可能无法整洁的整合入历史,因为补丁中的修改已经存在于某个地方了)。

另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。
或者先 git fetch,再 git rebase teamone/master
git config --global pull.rebase true 更改 pull.rebase 的默认配置。

只要你把变基命令当作是在推送前清理提交使之整洁的工具,并且只在从未推送至共用仓库的提交上执行变基命令,就不会有事。 如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull –rebase 命令,这样尽管不能避免伤痛,但能有所缓解。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作