Git专题--系统的学习Git之四

本文为整理自:伯乐在线

1.Git详解之一:Git起步

2.Git详解之二:Git基础

3.Git详解之三:Git分支

4.Git详解之四:服务器上的Git

5.Git详解之五:分布式Git

6.Git详解之六:Git工具

7.Git详解之七:自定义Git

8.Git详解之八:Git与其他系统

9.Git详解之九:Git内部原理

Git详解之八:Git与其他系统

Git 与其他系统

世界不是完美的。大多数时候,将所有接触到的项目全部转向 Git 是不可能的。有时我们不得不为某个项目使用其他的版本控制系统(VCS, Version Control System ),其中比较常见的是 Subversion 。你将在本章的第一部分学习使用git svn ,Git
为 Subversion 附带的双向桥接工具。(伯乐在线注:如果你对Git还不了解,建议从本Git系列第一篇文章开始阅读)

或许现在你已经在考虑将先前的项目转向 Git 。本章的第二部分将介绍如何将项目迁移到 Git:先介绍从 Subversion 的迁移,然后是 Perforce,最后介绍如何使用自定义的脚本进行非标准的导入。

8.1  Git 与 Subversion

当前,大多数开发中的开源项目以及大量的商业项目都使用 Subversion 来管理源码。作为最流行的开源版本控制系统,Subversion 已经存在了接近十年的时间。它在许多方面与 CVS 十分类似,后者是前者出现之前代码控制世界的霸主。

Git 最为重要的特性之一是名为 git svn 的 Subversion 双向桥接工具。该工具把 Git 变成了 Subversion 服务的客户端,从而让你在本地享受到 Git 所有的功能,而后直接向 Subversion 服务器推送内容,仿佛在本地使用了
Subversion 客户端。也就是说,在其他人忍受古董的同时,你可以在本地享受分支合并,使暂存区域,衍合以及 单项挑拣等等。这是个让 Git 偷偷潜入合作开发环境的好东西,在帮助你的开发同伴们提高效率的同时,它还能帮你劝说团队让整个项目框架转向对 Git 的支持。这个 Subversion 之桥是通向分布式版本控制系统(DVCS, Distributed VCS )世界的神奇隧道。

git svn

Git 中所有 Subversion 桥接命令的基础是 git svn 。所有的命令都从它开始。相关的命令数目不少,你将通过几个简单的工作流程了解到其中常见的一些。

值得警戒的是,在使用 git svn 的时候,你实际是在与 Subversion 交互,Git 比它要高级复杂的多。尽管可以在本地随意的进行分支和合并,最好还是通过衍合保持线性的提交历史,尽量避免类似与远程 Git 仓库动态交互这样的操作。

避免修改历史再重新推送的做法,也不要同时推送到并行的 Git 仓库来试图与其他 Git 用户合作。Subersion 只能保存单一的线性提交历史,一不小心就会被搞糊涂。合作团队中同时有人用 SVN 和 Git,一定要确保所有人都使用 SVN 服务来协作——这会让生活轻松很多。

初始设定

为了展示功能,先要一个具有写权限的 SVN 仓库。如果想尝试这个范例,你必须复制一份其中的测试仓库。比较简单的做法是使用一个名为 svnsync 的工具。较新的 Subversion 版本中都带有该工具,它将数据编码为用于网络传输的格式。

要尝试本例,先在本地新建一个 Subversion 仓库:


1

2

$mkdir/tmp/test-svn

$
svnadmin create
/tmp/test-svn

然后,允许所有用户修改 revprop —— 简单的做法是添加一个总是以 0 作为返回值的 pre-revprop-change 脚本:


1

2

3

4

$cat/tmp/test-svn/hooks/pre-revprop-change

#!/bin/sh

exit0;

$chmod+x/tmp/test-svn/hooks/pre-revprop-change

现在可以调用 svnsync init 加目标仓库,再加源仓库的格式来把该项目同步到本地了:


1

$
svnsync init
file:///tmp/test-svnhttp://progit-example.googlecode.com/svn/

这将建立进行同步所需的属性。可以通过运行以下命令来克隆代码:


1

2

3

4

5

6

7

$
svnsync
syncfile:///tmp/test-svn

Committed
revision 1.

Copied
properties
forrevision
1.

Committed
revision 2.

Copied
properties
forrevision
2.

Committed
revision 3.

...

别看这个操作只花掉几分钟,要是你想把源仓库复制到另一个远程仓库,而不是本地仓库,那将花掉接近一个小时,尽管项目中只有不到 100 次的提交。 Subversion 每次只复制一次修改,把它推送到另一个仓库里,然后周而复始——惊人的低效,但是我们别无选择。

入门

有了可以写入的 Subversion 仓库以后,就可以尝试一下典型的工作流程了。我们从 git svn clone 命令开始,它会把整个 Subversion 仓库导入到一个本地的 Git 仓库中。提醒一下,这里导入的是一个货真价实的 Subversion
仓库,所以应该把下面的file:///tmp/test-svn 换成你所用的 Subversion 仓库的 URL:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

$
git svn clone
file:///tmp/test-svn-T
trunk -b branches -t tags

Initialized
empty Git repository
in/Users/schacon/projects/testsvnsync/svn/.git/

r1
= b4e387bc68740b5af56c2a5faf4003ae42bd135c (trunk)

      A   
m4
/acx_pthread.m4

      A   
m4
/stl_hash.m4

...

r75
= d1957f3b307922124eec6314e15bcda59e3d9610 (trunk)

Found
possible branch point:
file:///tmp/test-svn/trunk=>
\

    file:///tmp/test-svn/branches/my-calc-branch,
75

Found
branch parent: (my-calc-branch) d1957f3b307922124eec6314e15bcda59e3d9610

Following
parent with do_switch

Successfully
followed parent

r76
= 8624824ecc0badd73f40ea2f01fce51894189b01 (my-calc-branch)

Checked
out HEAD:

 file:///tmp/test-svn/branches/my-calc-branchr76

这相当于针对所提供的 URL 运行了两条命令—— git svn init 加上 gitsvn
fetch
 。可能会花上一段时间。我们所用的测试项目仅仅包含 75 次提交并且它的代码量不算大,所以只有几分钟而已。不过,Git 仍然需要提取每一个版本,每次一个,再逐个提交。对于一个包含成百上千次提交的项目,花掉的时间则可能是几小时甚至数天。

-T trunk -b branches -t tags 告诉 Git 该 Subversion 仓库遵循了基本的分支和标签命名法则。如果你的主干(译注:trunk,相当于非分布式版本控制里的master分支,代表开发的主线),分支或者标签以不同的方式命名,则应做出相应改变。由于该法则的常见性,可以使用-s 来代替整条命令,它意味着标准布局(s
是 Standard layout 的首字母),也就是前面选项的内容。下面的命令有相同的效果:


1

$
git svn clone
file:///tmp/test-svn-s

现在,你有了一个有效的 Git 仓库,包含着导入的分支和标签:


1

2

3

4

5

6

7

8

$
git branch -a

*
master

  my-calc-branch

  tags/2.0.2

  tags/release-2.0.1

  tags/release-2.0.2

  tags/release-2.0.2rc1

  trunk

值得注意的是,该工具分配命名空间时和远程引用的方式不尽相同。克隆普通的 Git 仓库时,可以以 origin/[branch] 的形式获取远程服务器上所有可用的分支——分配到远程服务的名称下。然而git
svn
 假定不存在多个远程服务器,所以把所有指向远程服务的引用不加区分的保存下来。可以用 Git 探测命令 show-ref 来查看所有引用的全名。


1

2

3

4

5

6

7

8

$
git show-ref

1cbd4904d9982f386d87f88fce1c24ad7c0f0471
refs
/heads/master

aee1ecc26318164f355a883f5d99cff0c852d3c4
refs
/remotes/my-calc-branch

03d09b0e2aad427e34a6d50ff147128e76c0e0f5
refs
/remotes/tags/2.0.2

50d02cc0adc9da4319eeba0900430ba219b9c376
refs
/remotes/tags/release-2.0.1

4caaa711a50c77879a91b8b90380060f672745cb
refs
/remotes/tags/release-2.0.2

1c4cb508144c513ff1214c3488abe66dcb92916f
refs
/remotes/tags/release-2.0.2rc1

1cbd4904d9982f386d87f88fce1c24ad7c0f0471
refs
/remotes/trunk

而普通的 Git 仓库应该是这个模样:


1

2

3

4

5

$
git show-ref

83e38c7a0af325a9722f2fdc56b10188806d83a1
refs
/heads/master

3e15e38c198baac84223acfc6224bb8b99ff2281
refs
/remotes/gitserver/master

0a30dd3b0c795b80212ae723640d4e5d48cabdff
refs
/remotes/origin/master

25812380387fdd55f916652be4881c6f11600d6f
refs
/remotes/origin/testing

这里有两个远程服务器:一个名为 gitserver ,具有一个 master分支;另一个叫 origin,具有 master 和 testing 两个分支。

注意本例中通过 git svn 导入的远程引用,(Subversion 的)标签是当作远程分支添加的,而不是真正的 Git 标签。导入的 Subversion 仓库仿佛是有一个带有不同分支的 tags 远程服务器。

提交到 Subversion

有了可以开展工作的(本地)仓库以后,你可以开始对该项目做出贡献并向上游仓库提交内容了,Git 这时相当于一个 SVN 客户端。假如编辑了一个文件并进行提交,那么这次提交仅存在于本地的 Git 而非 Subversion 服务器上。


1

2

3

$
git commit -am
‘Adding
git-svn instructions to the README‘

[master
97031e5] Adding git-svn instructions to the README

 1
files changed, 1 insertions(+), 1 deletions(-)

接下来,可以将作出的修改推送到上游。值得注意的是,Subversion 的使用流程也因此改变了——你可以在离线状态下进行多次提交然后一次性的推送到 Subversion 的服务器上。向 Subversion 服务器推送的命令是git svn dcommit


1

2

3

4

5

6

7

8

$
git svn dcommit

Committing
to
file:///tmp/test-svn/trunk...

       M     
README.txt

Committed
r79

       M     
README.txt

r79
= 938b1a547c2cc92033b74d32030e86468294a5c8 (trunk)

No
changes between current HEAD and refs
/remotes/trunk

Resetting
to the latest refs
/remotes/trunk

所有在原 Subversion 数据基础上提交的 commit 会一一提交到 Subversion,然后你本地 Git 的 commit 将被重写,加入一个特别标识。这一步很重要,因为它意味着所有 commit 的 SHA-1 指都会发生变化。这也是同时使用 Git 和 Subversion 两种服务作为远程服务不是个好主意的原因之一。检视以下最后一个 commit,你会找到新添加的git-svn-id(译注:即本段开头所说的特别标识):


1

2

3

4

5

6

7

8

$
git log -1

commit
938b1a547c2cc92033b74d32030e86468294a5c8

Author:
schacon <[email protected]>

Date:  
Sat May 2 22:06:44 2009 +0000

    Adding
git-svn instructions to the README

    git-svn-id:file:///tmp/test-svn/trunk@79
4c93b258-373f-11de-be05-5f7a86268029

注意看,原本以 97031e5 开头的 SHA-1 校验值在提交完成以后变成了 938b1a5 。如果既要向
Git 远程服务器推送内容,又要推送到 Subversion 远程服务器,则必须先向 Subversion 推送(dcommit),因为该操作会改变所提交的数据内容。

拉取最新进展

如果要与其他开发者协作,总有那么一天你推送完毕之后,其他人发现他们推送自己修改的时候(与你推送的内容)产生冲突。这些修改在你合并之前将一直被拒绝。在 git svn 里这种情况形似:


1

2

3

4

5

$
git svn dcommit

Committing
to
file:///tmp/test-svn/trunk...

Merge
conflict during commit: Your
fileor
directory
‘README.txt‘is
probably \

out-of-date:
resource out of
date;
try updating at
/Users/schacon/libexec/git-\

core/git-svnline
482

为了解决该问题,可以运行 git svn rebase ,它会拉取服务器上所有最新的改变,再次基础上衍合你的修改:


1

2

3

4

5

$
git svn rebase

       M     
README.txt

r80
= ff829ab914e8775c7c025d741beb3d523ee30bc4 (trunk)

First,
rewinding
headto
replay your work on
topof
it...

Applying:
first user change

现在,你做出的修改都发生在服务器内容之后,所以可以顺利的运行 dcommit :


1

2

3

4

5

6

7

8

$
git svn dcommit

Committing
to
file:///tmp/test-svn/trunk...

       M     
README.txt

Committed
r81

       M     
README.txt

r81
= 456cbe6337abe49154db70106d1836bc1332deed (trunk)

No
changes between current HEAD and refs
/remotes/trunk

Resetting
to the latest refs
/remotes/trunk

需要牢记的一点是,Git 要求我们在推送之前先合并上游仓库中最新的内容,而 git svn 只要求存在冲突的时候才这样做。假如有人向一个文件推送了一些修改,这时你要向另一个文件推送一些修改,那么dcommit 将正常工作:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

$
git svn dcommit

Committing
to
file:///tmp/test-svn/trunk...

       M     
configure.ac

Committed
r84

       M     
autogen.sh

r83
= 8aa54a74d452f82eee10076ab2584c1fc424853b (trunk)

       M     
configure.ac

r84
= cdbac939211ccb18aa744e581e46563af5d962d0 (trunk)

W:
d2f23b80f67aaaa1f6f5aaef48fce3263ac71a92 and refs
/remotes/trunkdiffer,
\

  using
rebase:

:100755
100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 \

  015e4c98c482f0fa71e4d5434338014530b37fa6
M   autogen.sh

First,
rewinding
headto
replay your work on
topof
it...

Nothing
to
do.

这一点需要牢记,因为它的结果是推送之后项目处于一个不完整存在与任何主机上的状态。如果做出的修改无法兼容但没有产生冲突,则可能造成一些很难确诊的难题。这和使用 Git 服务器是不同的——在 Git 世界里,发布之前,你可以在客户端系统里完整的测试项目的状态,而在 SVN 永远都没法确保提交前后项目的状态完全一样。

及时还没打算进行提交,你也应该用这个命令从 Subversion 服务器拉取最新修改。sit svn fetch 能获取最新的数据,不过git
svn rebase
 才会在获取之后在本地进行更新 。


1

2

3

4

5

$
git svn rebase

       M     
generate_descriptor_proto.sh

r82
= bd16df9173e424c6f52c337ab6efa7f7643282f1 (trunk)

First,
rewinding
headto
replay your work on
topof
it...

Fast-forwarded
master to refs
/remotes/trunk.

不时地运行一下 git svn rebase 可以确保你的代码没有过时。不过,运行该命令时需要确保工作目录的整洁。如果在本地做了修改,则必须在运行git
svn rebase
 之前或暂存工作,或暂时提交内容——否则,该命令会发现衍合的结果包含着冲突因而终止。

Git 分支问题

习惯了 Git 的工作流程以后,你可能会创建一些特性分支,完成相关的开发工作,然后合并他们。如果要用 git svn 向 Subversion 推送内容,那么最好是每次用衍合来并入一个单一分支,而不是直接合并。使用衍合的原因是 Subversion 只有一个线性的历史而不像 Git 那样处理合并,所以 Git svn 在把快照转换为 Subversion 的 commit 时只能包含第一个祖先。

假设分支历史如下:创建一个 experiment 分支,进行两次提交,然后合并到 master 。在 dcommit 的时候会得到如下输出:


1

2

3

4

5

$
git svn rebase

       M     
generate_descriptor_proto.sh

r82
= bd16df9173e424c6f52c337ab6efa7f7643282f1 (trunk)

First,
rewinding
headto
replay your work on
topof
it...

Fast-forwarded
master to refs
/remotes/trunk.

在一个包含了合并历史的分支上使用 dcommit 可以成功运行,不过在 Git 项目的历史中,它没有重写你在 experiment 分支中的两个
commit ——另一方面,这些改变却出现在了 SVN 版本中同一个合并 commit 中。

在别人克隆该项目的时候,只能看到这个合并 commit 包含了所有发生过的修改;他们无法获知修改的作者和时间等提交信息。

Subversion 分支

Subversion 的分支和 Git 中的不尽相同;避免过多的使用可能是最好方案。不过,用 git svn 创建和提交不同的 Subversion 分支仍是可行的。

创建新的 SVN 分支

要在 Subversion 中建立一个新分支,需要运行 git svn branch [分支名] To create a new branch in Subversion, you rungit
svn branch [branchname]
:


1

2

3

4

5

6

7

8

$
git svn branch opera

Copyingfile:///tmp/test-svn/trunkat
r87 to
file:///tmp/test-svn/branches/opera...

Found
possible branch point:
file:///tmp/test-svn/trunk=>
\

  file:///tmp/test-svn/branches/opera,
87

Found
branch parent: (opera) 1f6bfe471083cbca06ac8d4176f7ad4de0d62e5f

Following
parent with do_switch

Successfully
followed parent

r89
= 9b6fe0b90c5c9adf9165f700897518dbc54a7cbf (opera)

相当于在 Subversion 中的 svn copy trunk branches/opera 命令并且对 Subversion 服务器进行了相关操作。值得提醒的是它没有检出和转换到那个分支;如果现在进行提交,将提交到服务器上的trunk
而非 opera

切换当前分支

Git 通过搜寻提交历史中 Subversion 分支的头部来决定 dcommit 的目的地——而它应该只有一个,那就是当前分支历史中最近一次包含 git-svn-id 的提交。

如果需要同时在多个分支上提交,可以通过导入 Subversion 上某个其他分支的 commit 来建立以该分支为 dcommit 目的地的本地分支。比如你想拥有一个并行维护的opera 分支,可以运行


1

$
git branch opera remotes
/opera

然后,如果要把 opera 分支并入 trunk (本地的 master 分支),可以使用普通的git
merge
。不过最好提供一条描述提交的信息(通过 -m),否则这次合并的记录是 Merge
branch opera
 ,而不是任何有用的东西。

记住,虽然使用了 git merge 来进行这次操作,并且合并过程可能比使用 Subversion 简单一些(因为 Git 会自动找到适合的合并基础),这并不是一次普通的 Git 合并提交。最终它将被推送回 commit 无法包含多个祖先的 Subversion
服务器上;因而在推送之后,它将变成一个包含了所有在其他分支上做出的改变的单一 commit。把一个分支合并到另一个分支以后,你没法像在 Git 中那样轻易的回到那个分支上继续工作。提交时运行的dcommit 命令擦除了全部有关哪个分支被并入的信息,因而以后的合并基础计算将是不正确的——
dcommit 让 git merge 的结果变得类似于git
merge --squash
。不幸的是,我们没有什么好办法来避免该情况—— Subversion 无法储存这个信息,所以在使用它作为服务器的时候你将永远为这个缺陷所困。为了不出现这种问题,在把本地分支(本例中的opera)并入 trunk
以后应该立即将其删除。

对应 Subversion 的命令

git svn 工具集合了若干个与 Subversion 类似的功能,对应的命令可以简化向 Git 的转化过程。下面这些命令能实现 Subversion 的这些功能。

SVN 风格的历史

习惯了 Subversion 的人可能想以 SVN 的风格显示历史,运行 git svn log 可以让提交历史显示为 SVN 格式:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

$
git svn log

------------------------------------------------------------------------

r87
| schacon | 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009) | 2 lines

autogen
change

------------------------------------------------------------------------

r86
| schacon | 2009-05-02 16:00:21 -0700 (Sat, 02 May 2009) | 2 lines

Merge
branch
‘experiment‘

------------------------------------------------------------------------

r85
| schacon | 2009-05-02 16:00:09 -0700 (Sat, 02 May 2009) | 2 lines

updated
the changelog

关于 git svn log ,有两点需要注意。首先,它可以离线工作,不像 svn
log
 命令,需要向 Subversion 服务器索取数据。其次,它仅仅显示已经提交到 Subversion 服务器上的 commit。在本地尚未 dcommit 的 Git 数据不会出现在这里;其他人向 Subversion 服务器新提交的数据也不会显示。等于说是显示了最近已知 Subversion 服务器上的状态。

SVN 日志

类似 git svn log 对 git
log
 的模拟,svn annotate 的等效命令是git
svn blame [文件名]
。其输出如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

$
git svn blame README.txt

 2  
temporal Protocol Buffers - Google‘s data interchange
format

 2  
temporal Copyright 2008 Google Inc.

 2  
temporal http:
//code.google.com/apis/protocolbuffers/

 2  
temporal

22  
temporal C++ Installation - Unix

22  
temporal =======================

 2  
temporal

79   
schacon Committing
ingit-svn.

78   
schacon

 2  
temporal To build and
installthe
C++ Protocol Buffer runtime and the Protocol

 2  
temporal Buffer compiler (protoc) execute the following:

 2  
temporal

同样,它不显示本地的 Git 提交以及 Subversion 上后来更新的内容。

SVN 服务器信息

还可以使用 git svn info 来获取与运行 svn
info
 类似的信息:


1

2

3

4

5

6

7

8

9

10

11

$
git svn info

Path:
.

URL:
https:
//schacon-test.googlecode.com/svn/trunk

Repository
Root: https:
//schacon-test.googlecode.com/svn

Repository
UUID: 4c93b258-373f-11de-be05-5f7a86268029

Revision:
87

Node
Kind: directory

Schedule:
normal

Last
Changed Author: schacon

Last
Changed Rev: 87

Last
Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009)

它与 blame 和 log 的相同点在于离线运行以及只更新到最后一次与
Subversion 服务器通信的状态。

略 Subversion 之所略

假如克隆了一个包含了 svn:ignore 属性的 Subversion 仓库,就有必要建立对应的 .gitignore 文件来防止意外提交一些不应该提交的文件。git
svn
 有两个有益于改善该问题的命令。第一个是git svn create-ignore,它自动建立对应的.gitignore 文件,以便下次提交的时候可以包含它。

第二个命令是 git svn show-ignore,它把需要放进 .gitignore 文件中的内容打印到标准输出,方便我们把输出重定向到项目的黑名单文件:


1

$
git svn show-ignore > .git
/info/exclude

这样一来,避免了 .gitignore 对项目的干扰。如果你是一个 Subversion 团队里唯一的 Git 用户,而其他队友不喜欢项目包含.gitignore,该方法是你的不二之选。

Git-Svn 总结

git svn 工具集在当前不得不使用 Subversion 服务器或者开发环境要求使用 Subversion 服务器的时候格外有用。不妨把它看成一个跛脚的 Git,然而,你还是有可能在转换过程中碰到一些困惑你和合作者们的迷题。为了避免麻烦,试着遵守如下守则:

● 保持一个不包含由 git merge 生成的 commit 的线性提交历史。将在主线分支外进行的开发通通衍合回主线;避免直接合并。

● 不要单独建立和使用一个 Git 服务来搞合作。可以为了加速新开发者的克隆进程建立一个,但是不要向它提供任何不包含 git-svn-id 条目的内容。甚至可以添加一个pre-receive 挂钩来在每一个提交信息中查找 git-svn-id 并拒绝提交那些不包含它的
commit。

如果遵循这些守则,在 Subversion 上工作还可以接受。然而,如果能迁徙到真正的 Git 服务器,则能为团队带来更多好处。

8.2  迁移到 Git

如果在其他版本控制系统中保存了某项目的代码而后决定转而使用 Git,那么该项目必须经历某种形式的迁移。本节将介绍 Git 中包含的一些针对常见系统的导入脚本,并将展示编写自定义的导入脚本的方法。

导入

你将学习到如何从专业重量级的版本控制系统中导入数据—— Subversion 和 Perforce —— 因为据我所知这二者的用户是(向 Git)转换的主要群体,而且 Git 为此二者附带了高质量的转换工具。

Subversion

读过前一节有关 git svn 的内容以后,你应该能轻而易举的根据其中的指导来 git
svn clone
 一个仓库了;然后,停止 Subversion 的使用,向一个新 Git server 推送,并开始使用它。想保留历史记录,所花的时间应该不过就是从 Subversion 服务器拉取数据的时间(可能要等上好一会就是了)。

然而,这样的导入并不完美;而且还要花那么多时间,不如干脆一次把它做对!首当其冲的任务是作者信息。在 Subversion,每个提交者在都在主机上有一个用户名,记录在提交信息中。上节例子中多处显示了schacon ,比如 blame的输出以及 git
svn log
。如果想让这条信息更好的映射到 Git 作者数据里,则需要 从 Subversion 用户名到 Git 作者的一个映射关系。建立一个叫做user.txt 的文件,用如下格式表示映射关系:


1

2

schacon
= Scott Chacon <[email protected] geemail.com>

selse
= Someo Nelse <[email protected] geemail.com>

通过该命令可以获得 SVN 作者的列表:


1

$
svn log --xml |
grepauthor
|
sort-u
| perl -pe
‘s/.&gt;(.?)&lt;./$1
= /‘

它将输出 XML 格式的日志——你可以找到作者,建立一个单独的列表,然后从 XML 中抽取出需要的信息。(显而易见,本方法要求主机上安装了grepsort 和perl.)然后把输出重定向到
user.txt 文件,然后就可以在每一项的后面添加相应的 Git 用户数据。

为 git svn 提供该文件可以然它更精确的映射作者数据。你还可以在 clone 或者 init后面添加--no-metadata 来阻止 git
svn
 包含那些 Subversion 的附加信息。这样 import 命令就变成了:


1

2

$
git-svn clone http:
//my-project.googlecode.com/svn/\

      --authors-file=users.txt
--no-metadata -s my_project

现在 my_project 目录下导入的 Subversion 应该比原来整洁多了。原来的 commit 看上去是这样:


1

2

3

4

5

6

7

8

commit
37efa680e8473b615de980fa935944215428a35a

Author:
schacon <[email protected]>

Date:  
Sun May 3 00:12:22 2009 +0000

    fixedinstall-
go to trunk

    git-svn-id:
https:
//my-project.googlecode.com/svn/trunk@94
4c93b258-373f-11de-

    be05-5f7a86268029

现在是这样:


1

2

3

4

5

commit
03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2

Author:
Scott Chacon <[email protected] geemail.com>

Date:  
Sun May 3 00:12:22 2009 +0000

    fixedinstall-
go to trunk

不仅作者一项干净了不少,git-svn-id 也就此消失了。

你还需要一点 post-import(导入后) 清理工作。最起码的,应该清理一下 git
svn
 创建的那些怪异的索引结构。首先要移动标签,把它们从奇怪的远程分支变成实际的标签,然后把剩下的分支移动到本地。

要把标签变成合适的 Git 标签,运行


1

2

$cp-Rf
.git
/refs/remotes/tags/*
.git
/refs/tags/

$rm-Rf
.git
/refs/remotes/tags

该命令将原本以 tag/ 开头的远程分支的索引变成真正的(轻巧的)标签。

接下来,把 refs/remotes 下面剩下的索引变成本地分支:


1

2

$cp-Rf
.git
/refs/remotes/*
.git
/refs/heads/

$rm-Rf
.git
/refs/remotes

现在所有的旧分支都变成真正的 Git 分支,所有的旧标签也变成真正的 Git 标签。最后一项工作就是把新建的 Git 服务器添加为远程服务器并且向它推送。下面是新增远程服务器的例子:


1

$
git remote add origin [email protected]:myrepository.git

为了让所有的分支和标签都得到上传,我们使用这条命令:


1

$
git push origin --all

所有的分支和标签现在都应该整齐干净的躺在新的 Git 服务器里了。

Perforce

你将了解到的下一个被导入的系统是 Perforce. Git 发行的时候同时也附带了一个 Perforce 导入脚本,不过它是包含在源码的 contrib 部分——而不像git
svn
 那样默认可用。运行它之前必须获取 Git 的源码,可以在 git.kernel.org 下载:


1

2

$
git clone git:
//git.kernel.org/pub/scm/git/git.git

$cdgit/contrib/fast-import

在这个 fast-import 目录下,应该有一个叫做 git-p4 的
Python 可执行脚本。主机上必须装有 Python 和p4 工具该导入才能正常进行。例如,你要从 Perforce 公共代码仓库(译注: Perforce Public Depot,Perforce 官方提供的代码寄存服务)导入 Jam 工程。为了设定客户端,我们要把
P4PORT 环境变量 export 到 Perforce 仓库:


1

$exportP4PORT=public.perforce.com:1666

运行 git-p4 clone 命令将从 Perforce 服务器导入 Jam 项目,我们需要给出仓库和项目的路径以及导入的目标路径:


1

2

3

4

5

$
git-p4 clone
//public/jam/src@all/opt/p4import

Importing
from
//public/jam/src@all
into
/opt/p4import

Reinitialized
existing Git repository
in/opt/p4import/.git/

Import
destination: refs
/remotes/p4/master

Importing
revision 4409 (100%)

现在去 /opt/p4import 目录运行一下 git
log
 ,就能看到导入的成果:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

$
git log -2

commit
1fd4ec126171790efd2db83548b85b1bbbc07dc2

Author:
Perforce staff <[email protected] perforce.com>

Date:  
Thu Aug 19 10:18:45 2004 -0800

    Drop‘rc3‘moniker
of jam-2.5.  Folded rc2 and rc3 RELNOTES into

    the
main part of the document.  Built new
tar/zipballs.

    Only
16 months later.

    [git-p4:
depot-paths =
"//public/jam/src/":
change = 4409]

commit
ca8870db541a23ed867f38847eda65bf4363371d

Author:
Richard Geiger <[email protected] perforce.com>

Date:  
Tue Apr 22 20:51:34 2003 -0800

    Update
derived jamgram.c

    [git-p4:
depot-paths =
"//public/jam/src/":
change = 3108]

每一个 commit 里都有一个 git-p4 标识符。这个标识符可以保留,以防以后需要引用 Perforce 的修改版本号。然而,如果想删除这些标识符,现在正是时候——在开启新仓库之前。可以通过git
filter-branch
 来批量删除这些标识符:


1

2

3

4

5

$
git filter-branch --msg-filter ‘

        sed
-e
"/^$$!git-p4:/d"

Rewrite
1fd4ec126171790efd2db83548b85b1bbbc07dc2 (
123/123)

Ref‘refs/heads/master‘was
rewritten

现在运行一下 git log,你会发现这些 commit 的 SHA-1 校验值都发生了改变,而那些 git-p4 字串则从提交信息里消失了:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

$
git log -
2

commit
10a16d60cffca14d454a15c6164378f4082bc5b0

Author:
Perforce staff <[email protected] perforce.com>

Date:  
Thu Aug
1910:18:452004-0800

    Drop‘rc3‘moniker
of jam-
2.5
Folded rc2 and rc3 RELNOTES into

    the
main part of the document.  Built
newtar/zip
balls.

    Only16months
later.

commit
2b6c6db311dd76c34c66ec1c40a49405e6b527b2

Author:
Richard Geiger <[email protected] perforce.com>

Date:  
Tue Apr
2220:51:342003-0800

    Update
derived jamgram.c

至此导入已经完成,可以开始向新的 Git 服务器推送了。

自定导入脚本

如果先前的系统不是 Subversion 或 Perforce 之一,先上网找一下有没有与之对应的导入脚本——导入 CVS,Clear Case,Visual Source Safe,甚至存档目录的导入脚本已经存在。假如这些工具都不适用,或者使用的工具很少见,抑或你需要导入过程具有更多可制定性,则应该使用git
fast-import
。该命令从标准输入读取简单的指令来写入具体的 Git 数据。这样创建 Git 对象比运行纯 Git 命令或者手动写对象要简单的多(更多相关内容见第九章)。通过它,你可以编写一个导入脚本来从导入源读取必要的信息,同时在标准输出直接输出相关指示。你可以运行该脚本并把它的输出管道连接到git
fast-import

下面演示一下如何编写一个简单的导入脚本。假设你在进行一项工作,并且按时通过把工作目录复制为以时间戳back_YY_MM_DD 命名的目录来进行备份,现在你需要把它们导入 Git 。目录结构如下:


1

2

3

4

5

6

$ls/opt/import_from

back_2009_01_02

back_2009_01_04

back_2009_01_14

back_2009_02_03

current

为了导入到一个 Git 目录,我们首先回顾一下 Git 储存数据的方式。你可能还记得,Git 本质上是一个 commit 对象的链表,每一个对象指向一个内容的快照。而这里需要做的工作就是告诉fast-import 内容快照的位置,什么样的 commit 数据指向它们,以及它们的顺序。我们采取一次处理一个快照的策略,为每一个内容目录建立对应的
commit ,每一个 commit 与之前的建立链接。

正如在第七章 “Git 执行策略一例” 一节中一样,我们将使用 Ruby 来编写这个脚本,因为它是我日常使用的语言而且阅读起来简单一些。你可以用任何其他熟悉的语言来重写这个例子——它仅需要把必要的信息打印到标准输出而已。同时,如果你在使用 Windows,这意味着你要特别留意不要在换行的时候引入回车符(译注:carriage returns,Windows 换行时加入的符号,通常说的\r )——
Git 的 fast-import 对仅使用换行符(LF)而非 Windows 的回车符(CRLF)要求非常严格。

首先,进入目标目录并且找到所有子目录,每一个子目录将作为一个快照被导入为一个 commit。我们将依次进入每一个子目录并打印所需的命令来导出它们。脚本的主循环大致是这样:


1

2

3

4

5

6

7

8

9

10

11

12

13

last_mark
= nil

#
循环遍历所有目录

Dir.chdir(ARGV[0])do

  Dir.glob("*").eachdo|dir|

    nextifFile.file?(dir)

    #
进入目标目录

    Dir.chdir(dir)do

      last_mark
= print_export(dir, last_mark)

    end

  end

end

我们在每一个目录里运行 print_export ,它会取出上一个快照的索引和标记并返回本次快照的索引和标记;由此我们就可以正确的把二者连接起来。”标记(mark)” 是fast-import<中对 commit 标识符的叫法;在创建 commit 的同时,我们逐一赋予一个标记以便以后在把它连接到其他 commit 时使用。因此,在print_export方法中要做的第一件事就是根据目录名生成一个标记:


1

mark
= convert_dir_to_mark(dir)

实现该函数的方法是建立一个目录的数组序列并使用数组的索引值作为标记,因为标记必须是一个整数。这个方法大致是这样的:


1

2

3

4

5

6

7

$marks
= []

def
convert_dir_to_mark(dir)

  if!$marks.include?(dir)

    $marks
<< dir

  end

  ($marks.index(dir)
+
1).to_s

end

有了整数来代表每个 commit,我们现在需要提交附加信息中的日期。由于日期是用目录名表示的,我们就从中解析出来。print_export 文件的下一行将是:


1

date=convert_dir_to_date(dir)

而 convert_dir_to_date 则定义为


1

2

3

4

5

6

7

8

9

def
convert_dir_to_date(
dir)

  ifdir==‘current‘

    returnTime.now().to_i

  else

    dir=dir.gsub(‘back_‘,‘‘)

    (year,
month, day) =
dir.split(‘_‘)

    returnTime.local(year,
month, day).to_i

  end

end

它为每个目录返回一个整型值。提交附加信息里最后一项所需的是提交者数据,我们在一个全局变量中直接定义之:


1

$author
=
‘Scott
Chacon <[email protected] example.com>‘

我们差不多可以开始为导入脚本输出提交数据了。第一项信息指明我们定义的是一个 commit 对象以及它所在的分支,随后是我们生成的标记,提交者信息以及提交备注,然后是前一个 commit 的索引,如果有的话。代码大致这样:


1

2

3

4

5

6

#
打印导入所需的信息

puts‘commit
refs/heads/master‘

puts‘mark
:‘

+ mark

puts"committer
#{$author} #{date} -0700"

export_data(‘imported
from ‘

+ dir)

puts‘from
:‘

+ last_mark
iflast_mark

时区(-0700)处于简化目的使用硬编码。如果是从其他版本控制系统导入,则必须以变量的形式指明时区。 提交备注必须以特定格式给出:


1

data
(size)\n(contents)

该格式包含了单词 data,所读取数据的大小,一个换行符,最后是数据本身。由于随后指明文件内容的时候要用到相同的格式,我们写一个辅助方法,export_data


1

2

3

def
export_data
(string)

  print"data
#{string.size}\n#{string}"

end

唯一剩下的就是每一个快照的内容了。这简单的很,因为它们分别处于一个目录——你可以输出 deleeall 命令,随后是目录中每个文件的内容。Git 会正确的记录每一个快照:


1

2

3

4

puts‘deleteall‘

Dir.glob("**/*").eachdo|file|
next
if!File.file?(file)

  inline_data(file)

end

注意:由于很多系统把每次修订看作一个 commit 到另一个 commit 的变化量,fast-import 也可以依据每次提交获取一个命令来指出哪些文件被添加,删除或者修改过,以及修改的内容。我们将需要计算快照之间的差别并且仅仅给出这项数据,不过该做法要复杂很多——还如不直接把所有数据丢给 Git 然它自己搞清楚。假如前面这个方法更适用于你的数据,参考fast-import 的
man 帮助页面来了解如何以这种方式提供数据。

列举新文件内容或者指明带有新内容的已修改文件的格式如下:


1

2

3

M644inline
path/to/file

data
(size)

(file
contents)

这里,644 是权限模式(加入有可执行文件,则需要探测之并设定为 755),而 inline 说明我们在本行结束之后立即列出文件的内容。我们的 inline_data 方法大致是:


1

2

3

4

5

def
inline_data(file, code =
‘M‘,
mode =
‘644‘)

  content
= File.read(file)

  puts"#{code}
#{mode} inline #{file}"

  export_data(content)

end

我们重用了前面定义过的 export_data,因为这里和指明提交注释的格式如出一辙。

最后一项工作是返回当前的标记以便下次循环的使用。


1

returnmark

注意:如果你在用 Windows,一定记得添加一项额外的步骤。前面提过,Windows 使用 CRLF 作为换行字符而 Git fast-import 只接受 LF。为了绕开这个问题来满足 git fast-import,你需要让 ruby 用 LF 取代 CRLF:


1

$stdout.binmode

搞定了。现在运行该脚本,你将得到如下内容:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

$
ruby
import.rb
/opt/import_from

commit
refs/heads/master

mark
:
1

committer
Scott Chacon <[email protected] geemail.com>
1230883200-0700

data29

imported
from back_2009_01_02deleteall

M644inline
file.rb

data12

version
two

commit
refs/heads/master

mark
:
2

committer
Scott Chacon <[email protected] geemail.com>
1231056000-0700

data29

imported
from back_2009_01_04from :
1

deleteall

M644inline
file.rb

data14

version
three

M644inlinenew.rb

data16

newversion
one

(...)

要运行导入脚本,在需要导入的目录把该内容用管道定向到 git fast-import。你可以建立一个空目录然后运行 git
init
作为开头,然后运行该脚本:


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

$
git init

Initialized
empty Git repository
in/opt/import_to/.git/

$
ruby
import.rb
/opt/import_from | git fast-
import

git-fast-importstatistics:

---------------------------------------------------------------------

Alloc‘d
objects:      
5000

Total
objects:          
18(        1duplicates                 
)

      blobs 
:           
7(        1duplicates         0deltas)

      trees 
:           
6(        0duplicates         1deltas)

      commits:           5(        0duplicates         0deltas)

      tags  
:           
0(        0duplicates         0deltas)

Total
branches:          
1(        1loads    
)

      marks:          1024(        5unique   
)

      atoms:             3

Memory
total:         
2255KiB

       pools:         2098KiB

     objects:          156KiB

---------------------------------------------------------------------

pack_report:
getpagesize()            =      
4096

pack_report:
core.packedGitWindowSize =  
33554432

pack_report:
core.packedGitLimit      = 
268435456

pack_report:
pack_used_ctr            =         
9

pack_report:
pack_mmap_calls          =         
5

pack_report:
pack_open_windows        =         
1/         1

pack_report:
pack_mapped              =      
1356/      1356

---------------------------------------------------------------------

你会发现,在它成功执行完毕以后,会给出一堆有关已完成工作的数据。上例在一个分支导入了5次提交数据,包含了18个对象。现在可以运行 git log 来检视新的历史:


1

2

3

4

5

6

7

8

9

10

11

12

$
git log -
2

commit
10bfe7d22ce15ee25b60a824c8982157ca593d41

Author:
Scott Chacon <[email protected] example.com>

Date:  
Sun May
312:57:392009-0700

    imported
from current

commit
7e519590de754d079dd73b44d695a42c9d2df452

Author:
Scott Chacon <[email protected] example.com>

Date:  
Tue Feb
301:00:002009-0700

    imported
from back_2009_02_03

就它了——一个干净整洁的 Git 仓库。需要注意的是此时没有任何内容被检出——刚开始当前目录里没有任何文件。要获取它们,你得转到 master 分支的所在:


1

2

3

4

5

$ls

$
git reset --hard master

HEAD
is now at 10bfe7d imported from current

$ls

file.rb 
lib

fast-import 还可以做更多——处理不同的文件模式,二进制文件,多重分支与合并,标签,进展标识等等。一些更加复杂的实例可以在 Git 源码的contib/fast-import 目录里找到;其中较为出众的是前面提过的 git-p4 脚本。

8.3  总结

现在的你应该掌握了在 Subversion 上使用 Git 以及把几乎任何先存仓库无损失的导入为 Git 仓库。下一章将介绍 Git 内部的原始数据格式,从而是使你能亲手锻造其中的每一个字节,如果必要的话。

Git详解之九:Git内部原理

Git 内部原理

不管你是从前面的章节直接跳到了本章,还是读完了其余各章一直到这,你都将在本章见识 Git 的内部工作原理和实现方式。我个人发现学习这些内容对于理解 Git 的用处和强大是非常重要的,不过也有人认为这些内容对于初学者来说可能难以理解且过于复杂。正因如此我把这部分内容放在最后一章,你在学习过程中可以先阅 读这部分,也可以晚点阅读这部分,这完全取决于你自己。(伯乐在线注:如果你对Git还不了解,建议从本Git系列第一篇文章开始阅读)

既然已经读到这了,就让我们开始吧。首先要弄明白一点,从根本上来讲 Git 是一套内容寻址 (content-addressable) 文件系统,在此之上提供了一个 VCS 用户界面。马上你就会学到这意味着什么。

早期的 Git (主要是 1.5 之前版本) 的用户界面要比现在复杂得多,这是因为它更侧重于成为文件系统而不是一套更精致的 VCS 。最近几年改进了 UI 从而使它跟其他任何系统一样清晰易用。即便如此,还是经常会有一些陈腔滥调提到早期 Git 的 UI 复杂又难学。

内容寻址文件系统这一层相当酷,在本章中我会先讲解这部分。随后你会学到传输机制和最终要使用的各种库管理任务。

9.1  底层命令 (Plumbing) 和高层命令 (Porcelain)

本书讲解了使用 checkoutbranchremote 等共约
30 个 Git 命令。然而由于 Git 一开始被设计成供 VCS 使用的工具集而不是一整套用户友好的 VCS,它还包含了许多底层命令,这些命令用于以 UNIX 风格使用或由脚本调用。这些命令一般被称为 “plumbing” 命令(底层命令),其他的更友好的命令则被称为 “porcelain” 命令(高层命令)。

本书前八章主要专门讨论高层命令。本章将主要讨论底层命令以理解 Git 的内部工作机制、演示 Git 如何及为何要以这种方式工作。这些命令主要不是用来从命令行手工使用的,更多的是用来为其他工具和自定义脚本服务的。

当你在一个新目录或已有目录内执行 git init 时,Git 会创建一个 .git 目录,几乎所有
Git 存储和操作的内容都位于该目录下。如果你要备份或复制一个库,基本上将这一目录拷贝至其他地方就可以了。本章基本上都讨论该目录下的内容。该目录结构如下:


1

2

3

4

5

6

7

8

9

10

$
ls

HEAD

branches/

config

description

hooks/

index

info/

objects/

refs/

该目录下有可能还有其他文件,但这是一个全新的 git init 生成的库,所以默认情况下这些就是你能看到的结构。新版本的 Git 不再使用branches 目录,description 文件仅供
GitWeb 程序使用,所以不用关心这些内容。config 文件包含了项目特有的配置选项,info 目录保存了一份不希望在
.gitignore 文件中管理的忽略模式 (ignored patterns) 的全局可执行文件。hooks 目录包住了第六章详细介绍了的客户端或服务端钩子脚本。

另外还有四个重要的文件或目录:HEAD 及 index 文件,objects 及refs 目录。这些是
Git 的核心部分。objects 目录存储所有数据内容,refs 目录存储指向数据
(分支) 的提交对象的指针,HEAD 文件指向当前分支,index 文件保存了暂存区域信息。马上你将详细了解
Git 是如何操纵这些内容的。

9.2  Git 对象

Git 是一套内容寻址文件系统。很不错。不过这是什么意思呢?这种说法的意思是,从内部来看,Git 是简单的 key-value 数据存储。它允许插入任意类型的内容,并会返回一个键值,通过该键值可以在任何时候再取出该内容。可以通过底层命令hash-object 来示范这点,传一些数据给该命令,它会将数据保存在 .git 目录并返回表示这些数据的键值。首先初使化一个
Git 仓库并确认objects 目录是空的:


1

2

3

4

5

6

7

8

9

10

$
mkdir test

$
cd test

$
git init

Initialized
empty Git repository
in/tmp/test/.git/

$
find .git/objects

.git/objects

.git/objects/info

.git/objects/pack

$
find .git/objects -type f

$

Git 初始化了 objects 目录,同时在该目录下创建了 pack 和 info 子目录,但是该目录下没有其他常规文件。我们往这个
Git 数据库里存储一些文本:



1

2

$
echo
‘test
content‘

| git hash-object -w --stdin

d670460b4b4aece5915caf5c68d12f560a9fe3e4


参数 -w 指示 hash-object 命令存储
(数据) 对象,若不指定这个参数该命令仅仅返回键值。--stdin 指定从标准输入设备 (stdin) 来读取内容,若不指定这个参数则需指定一个要存储的文件的路径。该命令输出长度为 40 个字符的校验和。这是个 SHA-1 哈希值──其值为要存储的数据加上你马上会了解到的一种头信息的校验和。现在可以查看到
Git 已经存储了数据:


1

2

$
find .git/objects -type f

.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

可以在 objects 目录下看到一个文件。这便是 Git 存储数据内容的方式──为每份内容生成一个文件,取得该内容与头信息的 SHA-1 校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下 38 个字符为文件命名 (保存至子目录下)。

通过 cat-file 命令可以将数据内容取回。该命令是查看 Git 对象的瑞士军刀。传入 -p 参数可以让该命令输出数据内容的类型:



1

2

$
git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4

test
content


可以往 Git 中添加更多内容并取回了。也可以直接添加文件。比方说可以对一个文件进行简单的版本控制。首先,创建一个新文件,并把文件内容存储到数据库中:


1

2

3

$
echo
‘version
1‘

> test.txt

$
git hash-object -w test.txt

83baae61804e65cc73a7201a7252750c76066a30

接着往该文件中写入一些新内容并再次保存:


1

2

3

$
echo
‘version
2‘

> test.txt

$
git hash-object -w test.txt

1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

数据库中已经将文件的两个新版本连同一开始的内容保存下来了:


1

2

3

4

$
find .git/objects -type f

.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a

.git/objects/83/baae61804e65cc73a7201a7252750c76066a30

.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

再将文件恢复到第一个版本:



1

2

3

$
git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt

$
cat test.txt

version1


或恢复到第二个版本:



1

2

3

$
git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt

$
cat test.txt

version2


需要记住的是几个版本的文件 SHA-1 值可能与实际的值不同,其次,存储的并不是文件名而仅仅是文件内容。这种对象类型称为 blob 。通过传递 SHA-1 值给cat-file -t 命令可以让 Git 返回任何对象的类型:



1

2

$
git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

blob


tree (树) 对象

接下去来看 tree 对象,tree 对象可以存储文件名,同时也允许存储一组文件。Git 以一种类似 UNIX 文件系统但更简单的方式来存储内容。所有内容以 tree 或 blob 对象存储,其中 tree 对象对应于 UNIX 中的目录,blob 对象则大致对应于 inodes 或文件内容。一个单独的 tree 对象包含一条或多条 tree 记录,每一条记录含有一个指向 blob 或子 tree 对象的 SHA-1 指针,并附有该对象的权限模式 (mode)、类型和文件名信息。以 simplegit 项目为例,最新的
tree 可能是这个样子:


1

2

3

4

$
git cat-file -p master^{tree}

100644blob
a906cb2a4a904a152e80877d4088654daad0c859      README

100644blob
8f94139338f9404f26296befa88755fc2598c289      Rakefile

040000tree
99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

master^{tree} 表示 branch 分支上最新提交指向的
tree 对象。请注意 lib 子目录并非一个 blob 对象,而是一个指向别一个 tree 对象的指针:



1

2

$
git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0

100644blob
47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb


从概念上来讲,Git 保存的数据如图 9-1 所示。

图 9-1. Git 对象模型的简化版

你可以自己创建 tree 。通常 Git 根据你的暂存区域或 index 来创建并写入一个 tree 。因此要创建一个 tree 对象的话首先要通过将一些文件暂存从而创建一个 index 。可以使用 plumbing 命令update-index 为一个单独文件
── test.txt 文件的第一个版本 ──    创建一个 index    。通过该命令人为的将 test.txt 文件的首个版本加入到了一个新的暂存区域中。由于该文件原先并不在暂存区域中 (甚至就连暂存区域也还没被创建出来呢) ,必须传入--add 参数;由于要添加的文件并不在当前目录下而是在数据库中,必须传入 --cacheinfo 参数。同时指定了文件模式,SHA-1
值和文件名:



1

2

$
git update-index --add --cacheinfo
100644\

  83baae61804e65cc73a7201a7252750c76066a30
test.txt


在本例中,指定了文件模式为 100644,表明这是一个普通文件。其他可用的模式有:100755 表示可执行文件,120000 表示符号链接。文件模式是从常规的
UNIX 文件模式中参考来的,但是没有那么灵活 ── 上述三种模式仅对 Git 中的文件 (blobs) 有效 (虽然也有其他模式用于目录和子模块)。

现在可以用 write-tree 命令将暂存区域的内容写到一个 tree 对象了。无需 -w 参数
── 如果目标 tree 不存在,调用write-tree 会自动根据 index 状态创建一个 tree 对象。



1

2

3

4

$
git write-tree

d8329fc1cc938780ffdd9f94e0d364e0ea74f579

$
git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579

100644blob
83baae61804e65cc73a7201a7252750c76066a30      test.txt


可以这样验证这确实是一个 tree 对象:



1

2

$
git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579

tree


再根据 test.txt 的第二个版本以及一个新文件创建一个新 tree 对象:



1

2

3

$
echo
‘new
file‘

>
new.txt

$
git update-index test.txt

$
git update-index --add
new.txt


这时暂存区域中包含了 test.txt 的新版本及一个新文件 new.txt 。创建 (写) 该 tree 对象 (将暂存区域或 index 状态写入到一个 tree 对象),然后瞧瞧它的样子:


1

2

3

4

5

$
git write-tree

0155eb4229851634a0f03eb265b69f5a2d56f341

$
git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341

100644blob
fa49b077972391ad58037050f2a75f74e3671e92     
new.txt

100644blob
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

请注意该 tree 对象包含了两个文件记录,且 test.txt 的 SHA 值是早先值的 “第二版” (1f7a7a)。来点更有趣的,你将把第一个 tree 对象作为一个子目录加进该 tree 中。可以用read-tree 命令将
tree 对象读到暂存区域中去。在这时,通过传一个 --prefix 参数给 read-tree,将一个已有的
tree 对象作为一个子 tree 读到暂存区域中:


1

2

3

4

5

6

7

$
git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579

$
git write-tree

3c4e9cd789d88d8d89c1073707c3585e41b0e614

$
git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614

040000tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak

100644blob
fa49b077972391ad58037050f2a75f74e3671e92     
new.txt

100644blob
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

如果从刚写入的新 tree 对象创建一个工作目录,将得到位于工作目录顶级的两个文件和一个名为 bak 的子目录,该子目录包含了 test.txt 文件的第一个版本。可以将 Git 用来包含这些内容的数据想象成如图 9-2 所示的样子。

图 9-2. 当前 Git 数据的内容结构

commit (提交) 对象

你现在有三个 tree 对象,它们指向了你要跟踪的项目的不同快照,可是先前的问题依然存在:必须记往三个 SHA-1 值以获得这些快照。你也没有关于谁、何时以及为何保存了这些快照的信息。commit 对象为你保存了这些基本信息。

要创建一个 commit 对象,使用 commit-tree 命令,指定一个 tree 的 SHA-1,如果有任何前继提交对象,也可以指定。从你写的第一个 tree 开始:



1

2

$
echo
‘first
commit‘

| git commit-tree d8329f

fdf4fc3344e67ab068f836878b6c4951e3b15f3d


通过 cat-file 查看这个新 commit 对象:


1

2

3

4

5

6

7

8

9

10

$
git cat-file -p fdf4fc3

tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579

author
Scott Chacon

      1243040974-0700

committer
Scott Chacon

       1243040974-0700

first
commit

commit 对象有格式很简单:指明了该时间点项目快照的顶层树对象、作者/提交者信息(从 Git 设理发店的 user.nameuser.email中获得)以及当前时间戳、一个空行,以及提交注释信息。

接着再写入另外两个 commit 对象,每一个都指定其之前的那个 commit 对象:



1

2

3

4

$
echo
‘second
commit‘

| git commit-tree 0155eb -p fdf4fc3

cac0cab538b970a37ea1e769cbbde608743bc96d

$
echo
‘third
commit‘
 
| git commit-tree 3c4e9c -p cac0cab

1a410efbd13591db07496601ebc7a059dd55cfe9


每一个 commit 对象都指向了你创建的树对象快照。出乎意料的是,现在已经有了真实的 Git 历史了,所以如果运行 git log 命令并指定最后那个 commit 对象的 SHA-1 便可以查看历史:


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

$
git log --stat 1a410e

commit
1a410efbd13591db07496601ebc7a059dd55cfe9

Author:
Scott Chacon

Date:  
Fri May
2218:15:242009-0700

    third
commit

 bak/test.txt
|   
1+

 1files
changed,
1insertions(+),0deletions(-)

commit
cac0cab538b970a37ea1e769cbbde608743bc96d

Author:
Scott Chacon

Date:  
Fri May
2218:14:292009-0700

    second
commit

 new.txt 
|   
1+

 test.txt
|   
2+-

 2files
changed,
2insertions(+),1deletions(-)

commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d

Author:
Scott Chacon

Date:  
Fri May
2218:09:342009-0700

    first
commit

 test.txt
|   
1+

 1files
changed,
1insertions(+),0deletions(-)

真棒。你刚刚通过使用低级操作而不是那些普通命令创建了一个 Git 历史。这基本上就是运行    git add 和 git
commit
命令时 Git 进行的工作    ──保存修改了的文件的 blob,更新索引,创建 tree 对象,最后创建 commit 对象,这些 commit 对象指向了顶层 tree 对象以及先前的 commit 对象。这三类 Git 对象 ── blob,tree 以及 tree ── 都各自以文件的方式保存在.git/objects 目录下。以下所列是目前为止样例中的所有对象,每个对象后面的注释里标明了它们保存的内容:


1

2

3

4

5

6

7

8

9

10

11

$
find .git/objects -type f

.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341
# tree
2

.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9
# commit
3

.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
# test.txt v2

.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614
# tree
3

.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
# test.txt v1

.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d
# commit
2

.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
#
‘test
content‘

.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579
# tree
1

.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92
#
new.txt

.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d
# commit
1

如果你按照以上描述进行了操作,可以得到如图 9-3 所示的对象图。

图 9-3. Git 目录下的所有对象

对象存储

之前我提到当存储数据内容时,同时会有一个文件头被存储起来。我们花些时间来看看 Git 是如何存储对象的。你将看来如何通过 Ruby 脚本语言存储一个 blob 对象 (这里以字符串 “what is up, doc?” 为例) 。使用irb 命令进入 Ruby
交互式模式:



1

2

3

$
irb

>>
content =
"what
is up, doc?"

=>"what
is up, doc?"


Git 以对象类型为起始内容构造一个文件头,本例中是一个 blob。然后添加一个空格,接着是数据内容的长度,最后是一个空字节 (null byte):



1

2

>>
header =
"blob
#{content.length}\0"

=>"blob
16\000"


Git 将文件头与原始数据内容拼接起来,并计算拼接后的新内容的 SHA-1 校验和。可以在 Ruby 中使用 require 语句导入 SHA1 digest 库,然后调用Digest::SHA1.hexdigest() 方法计算字符串的
SHA-1 值:


1

2

3

4

5

6

>>
store = header + content

=>"blob
16\000what is up, doc?"

>>
require
‘digest/sha1‘

=>true

>>
sha1 = Digest::SHA1.hexdigest(store)

=>"bd9dbf5aae1a3862dd1526723246b20206e5fc37"

Git 用 zlib 对数据内容进行压缩,在 Ruby 中可以用 zlib 库来实现。首先需要导入该库,然后用Zlib::Deflate.deflate() 对数据进行压缩:


1

2

3

4

>>
require
‘zlib‘

=>true

>>
zlib_content = Zlib::Deflate.deflate(store)

=>"x\234K\312\311OR04c(\317H,Q\310,V(-\320QH\311O\266\a\000_\034\a\235"

最后将用 zlib 压缩后的内容写入磁盘。需要指定保存对象的路径 (SHA-1 值的头两个字符作为子目录名称,剩余 38 个字符作为文件名保存至该子目录中)。在 Ruby 中,如果子目录不存在可以用FileUtils.mkdir_p() 函数创建它。接着用File.open 方法打开文件,并用 write() 方法将之前压缩的内容写入该文件:



1

2

3

4

5

6

7

8

>>
path =
‘.git/objects/‘+
sha1[
0,2]
+
‘/‘+
sha1[
2,38]

=>".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"

>>
require
‘fileutils‘

=>true

>>
FileUtils.mkdir_p(File.dirname(path))

=>".git/objects/bd"

>>
File.open(path,
‘w‘)
{ |f| f.write zlib_content }

=>32


这就行了 ── 你已经创建了一个正确的 blob 对象。所有的 Git 对象都以这种方式存储,惟一的区别是类型不同 ── 除了字符串 blob,文件头起始内容还可以是 commit 或 tree 。不过虽然 blob 几乎可以是任意内容,commit 和 tree 的数据却是有固定格式的。

9.3  Git References

你可以执行像 git log 1a410e 这样的命令来查看完整的历史,但是这样你就要记得 1a410e 是你最后一次提交,这样才能在提交历史中找到这些对象。你需要一个文件来用一个简单的名字来记录这些
SHA-1 值,这样你就可以用这些指针而不是原来的 SHA-1 值去检索了。

在 Git 中,我们称之为“引用”(references 或者 refs,译者注)。你可以在 .git/refs 目录下面找到这些包含 SHA-1 值的文件。在这个项目里,这个目录还没不包含任何文件,但是包含这样一个简单的结构:



1

2

3

4

5

6

$
find .git/refs

.git/refs

.git/refs/heads

.git/refs/tags

$
find .git/refs -type f

$


如果想要创建一个新的引用帮助你记住最后一次提交,技术上你可以这样做:


1

<code>$
echo
"1a410efbd13591db07496601ebc7a059dd55cfe9"&gt;
.git/refs/heads/master</code>

现在,你就可以在 Git 命令中使用你刚才创建的引用而不是 SHA-1 值:


1

2

3

4

$
git log --pretty=oneline  master

1a410efbd13591db07496601ebc7a059dd55cfe9
third commit

cac0cab538b970a37ea1e769cbbde608743bc96d
second commit

fdf4fc3344e67ab068f836878b6c4951e3b15f3d
first commit

当然,我们并不鼓励你直接修改这些引用文件。如果你确实需要更新一个引用,Git 提供了一个安全的命令 update-ref


1

$
git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9

基本上 Git 中的一个分支其实就是一个指向某个工作版本一条 HEAD 记录的指针或引用。你可以用这条命令创建一个指向第二次提交的分支:


1

$
git update-ref refs/heads/test cac0ca

这样你的分支将会只包含那次提交以及之前的工作:


1

2

3

$
git log --pretty=oneline test

cac0cab538b970a37ea1e769cbbde608743bc96d
second commit

fdf4fc3344e67ab068f836878b6c4951e3b15f3d
first commit

现在,你的 Git 数据库应该看起来像图 9-4 一样。

图 9-4. 包含分支引用的 Git 目录对象

每当你执行 git branch (分支名称) 这样的命令,Git 基本上就是执行 update-ref 命令,把你现在所在分支中最后一次提交的
SHA-1 值,添加到你要创建的分支的引用。

HEAD 标记

现在的问题是,当你执行 git branch (分支名称) 这条命令的时候,Git 怎么知道最后一次提交的 SHA-1 值呢?答案就是 HEAD 文件。HEAD 文件是一个指向你当前所在分支的引用标识符。这样的引用标识符——它看起来并不像一个普通的引用——其实并不包含
SHA-1 值,而是一个指向另外一个引用的指针。如果你看一下这个文件,通常你将会看到这样的内容:



1

2

$
cat .git/HEAD

ref:
refs/heads/master


如果你执行 git checkout test,Git 就会更新这个文件,看起来像这样:



1

2

$
cat .git/HEAD

ref:
refs/heads/test


当你再执行 git commit 命令,它就创建了一个 commit 对象,把这个 commit 对象的父级设置为 HEAD 指向的引用的 SHA-1 值。

你也可以手动编辑这个文件,但是同样有一个更安全的方法可以这样做:symbolic-ref。你可以用下面这条命令读取 HEAD 的值:



1

2

$
git symbolic-ref HEAD

refs/heads/master


你也可以设置 HEAD 的值:



1

2

3

$
git symbolic-ref HEAD refs/heads/test

$
cat .git/HEAD

ref:
refs/heads/test


但是你不能设置成 refs 以外的形式:



1

2

$
git symbolic-ref HEAD test

fatal:
Refusing to point HEAD outside of refs/


Tags

你刚刚已经重温过了 Git 的三个主要对象类型,现在这是第四种。Tag 对象非常像一个 commit 对象——包含一个标签,一组数据,一个消息和一个指针。最主要的区别就是 Tag 对象指向一个 commit 而不是一个 tree。它就像是一个分支引用,但是不会变化——永远指向同一个 commit,仅仅是提供一个更加友好的名字。

正如我们在第二章所讨论的,Tag 有两种类型:annotated 和 lightweight 。你可以类似下面这样的命令建立一个 lightweight tag:


1

$
git update-ref refs/tags/v1.
0cac0cab538b970a37ea1e769cbbde608743bc96d

这就是 lightweight tag 的全部 —— 一个永远不会发生变化的分支。 annotated tag 要更复杂一点。如果你创建一个 annotated tag,Git 会创建一个 tag 对象,然后写入一个指向指向它而不是直接指向 commit 的 reference。你可以这样创建一个 annotated tag(-a 参数表明这是一个
annotated tag):


1

$
git tag -a v1.
11a410efbd13591db07496601ebc7a059dd55cfe9
-m
‘test
tag‘

这是所创建对象的 SHA-1 值:



1

2

$
cat .git/refs/tags/v1.
1

9585191f37f7b0fb9444f35a9bf50de191beadc2


现在你可以运行 cat-file 命令检查这个 SHA-1 值:


1

2

3

4

5

6

7

8

9

$
git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2

object
1a410efbd13591db07496601ebc7a059dd55cfe9

type
commit

tag
v1.
1

tagger
Scott Chacon

      Sat
May
2316:48:582009-0700

test
tag

值得注意的是这个对象指向你所标记的 commit 对象的 SHA-1 值。同时需要注意的是它并不是必须要指向一个 commit 对象;你可以标记任何 Git 对象。例如,在 Git 的源代码里,管理者添加了一个 GPG 公钥(这是一个 blob 对象)对它做了一个标签。你就可以运行:


1

$
git cat-file blob junio-gpg-pub

来查看 Git 源代码仓库中的公钥. Linux kernel 也有一个不是指向 commit 对象的 tag —— 第一个 tag 是在导入源代码的时候创建的,它指向初始 tree (initial tree,译者注)。github

Remotes

你将会看到的第四种 reference 是 remote reference(远程引用,译者注)。如果你添加了一个 remote 然后推送代码过去,Git 会把你最后一次推送到这个 remote 的每个分支的值都记录在refs/remotes 目录下。例如,你可以添加一个叫做origin 的
remote 然后把你的 master 分支推送上去:


1

2

3

4

5

6

7

8

$
git remote add origin [email protected] github.com:schacon/simplegit-progit.git

$
git push origin master

Counting
objects:
11,
done.

Compressing
objects:
100%
(
5/5),
done.

Writing
objects:
100%
(
7/7),716bytes,
done.

Total7(delta2),
reused
4(delta1)

To
[email protected] github.com:schacon/simplegit-progit.git

   a11bef0..ca82a6d 
master -> master

然后查看 refs/remotes/origin/master 这个文件,你就会发现 origin remote
中的master 分支就是你最后一次和服务器的通信。


1

2

$
cat .git/refs/remotes/origin/master

ca82a6dff817ec66f44342007202690a93763949

Remote 应用和分支主要区别在于他们是不能被 check out 的。Git 把他们当作是标记这些了这些分支在服务器上最后状态的一种书签。

9.4  Packfiles

我们再来看一下 test Git 仓库。目前为止,有 11 个对象 ── 4 个 blob,3 个 tree,3 个 commit 以及一个 tag:


1

2

3

4

5

6

7

8

9

10

11

12

$
find .git/objects -type f

.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341
# tree
2

.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9
# commit
3

.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
# test.txt v2

.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614
# tree
3

.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
# test.txt v1

.git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2
# tag

.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d
# commit
2

.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
#
‘test
content‘

.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579
# tree
1

.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92
#
new.txt

.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d
# commit
1

Git 用 zlib 压缩文件内容,因此这些文件并没有占用太多空间,所有文件加起来总共仅用了 925 字节。接下去你会添加一些大文件以演示 Git 的一个很有意思的功能。将你之前用到过的 Grit 库中的 repo.rb 文件加进去 ── 这个源代码文件大小约为 12K:


1

2

3

4

5

6

7

8

$
curl http:
//github.com/mojombo/grit/raw/master/lib/grit/repo.rb
> repo.rb

$
git add repo.rb

$
git commit -m
‘added
repo.rb‘

[master
484a592] added repo.rb

 3files
changed,
459insertions(+),2deletions(-)

 deletemode100644bak/test.txt

 create
mode
100644repo.rb

 rewrite
test.txt (
100%)

如果查看一下生成的 tree,可以看到 repo.rb 文件的 blob 对象的 SHA-1 值:


1

2

3

4

$
git cat-file -p master^{tree}

100644blob
fa49b077972391ad58037050f2a75f74e3671e92     
new.txt

100644blob
9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e      repo.rb

100644blob
e3f094f522629ae358806b17daf78246c27c007b      test.txt

然后可以用 git cat-file 命令查看这个对象有多大:



1

2

$
git cat-file -s 9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e

12898


稍微修改一下些文件,看会发生些什么:


1

2

3

4

$
echo
‘#
testing‘

>> repo.rb

$
git commit -am
‘modified
repo a bit‘

[master
ab1afef] modified repo a bit

 1files
changed,
1insertions(+),0deletions(-)

查看这个 commit 生成的 tree,可以看到一些有趣的东西:


1

2

3

4

$
git cat-file -p master^{tree}

100644blob
fa49b077972391ad58037050f2a75f74e3671e92     
new.txt

100644blob
05408d195263d853f09dca71d55116663690c27c      repo.rb

100644blob
e3f094f522629ae358806b17daf78246c27c007b      test.txt

blob 对象与之前的已经不同了。这说明虽然只是往一个 400 行的文件最后加入了一行内容,Git 却用一个全新的对象来保存新的文件内容:



1

2

$
git cat-file -s 05408d195263d853f09dca71d55116663690c27c

12908


你的磁盘上有了两个几乎完全相同的 12K 的对象。如果 Git 只完整保存其中一个,并保存另一个对象的差异内容,岂不更好?

事实上 Git 可以那样做。Git 往磁盘保存对象时默认使用的格式叫松散对象 (loose object) 格式。Git 时不时地将这些对象打包至一个叫 packfile 的二进制文件以节省空间并提高效率。当仓库中有太多的松散对象,或是手工调用git gc 命令,或推送至远程服务器时,Git
都会这样做。手工调用 git gc 命令让 Git 将库中对象打包并看会发生些什么:


1

2

3

4

5

6

$
git gc

Counting
objects:
17,
done.

Delta
compression using
2threads.

Compressing
objects:
100%
(
13/13),
done.

Writing
objects:
100%
(
17/17),
done.

Total17(delta1),
reused
10(delta0)

查看一下 objects 目录,会发现大部分对象都不在了,与此同时出现了两个新文件:


1

2

3

4

5

6

$
find .git/objects -type f

.git/objects/71/08f7ecb345ee9d0084193f147cdad4d2998293

.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

.git/objects/info/packs

.git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.idx

.git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.pack

仍保留着的几个对象是未被任何 commit 引用的 blob ── 在此例中是你之前创建的 “what is up, doc?” 和 “test content” 这两个示例 blob。你从没将他们添加至任何 commit,所以 Git 认为它们是 “悬空” 的,不会将它们打包进 packfile 。

剩下的文件是新创建的 packfile 以及一个索引。packfile 文件包含了刚才从文件系统中移除的所有对象。索引文件包含了 packfile 的偏移信息,这样就可以快速定位任意一个指定对象。有意思的是运行gc 命令前磁盘上的对象大小约为 12K ,而这个新生成的
packfile 仅为 6K 大小。通过打包对象减少了一半磁盘使用空间。

Git 是如何做到这点的?Git 打包对象时,会查找命名及尺寸相近的文件,并只保存文件不同版本之间的差异内容。可以查看一下 packfile ,观察它是如何节省空间的。git verify-pack 命令用于显示已打包的内容:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

$
git verify-pack -v \

  .git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.idx

0155eb4229851634a0f03eb265b69f5a2d56f341
tree  
71765400

05408d195263d853f09dca71d55116663690c27c
blob  
129083478874

09f01cea547666f58d6a8d809583841a7c6f0130
tree  
1061075086

1a410efbd13591db07496601ebc7a059dd55cfe9
commit
225151322

1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob  
10195381

3c4e9cd789d88d8d89c1073707c3585e41b0e614
tree  
1011055211

484a59275031909e19aadb7c92262719cfcdf19a
commit
226153169

83baae61804e65cc73a7201a7252750c76066a30
blob  
10195362

9585191f37f7b0fb9444f35a9bf50de191beadc2
tag   
1361275476

9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e
blob  
7185193

1

05408d195263d853f09dca71d55116663690c27c
\

  ab1afef80fac8e34258ff41fc1b867c702daa24b
commit
23215712

cac0cab538b970a37ea1e769cbbde608743bc96d
commit
226154473

d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree  
36465316

e3f094f522629ae358806b17daf78246c27c007b
blob  
14867344352

f8f51d7d8a1760462eca26eebafde32087499533
tree  
106107749

fa49b077972391ad58037050f2a75f74e3671e92
blob  
918856

fdf4fc3344e67ab068f836878b6c4951e3b15f3d
commit
177122627

chain
length =
1:1object

pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.pack:
ok

如果你还记得的话, 9bc1d 这个 blob 是 repo.rb 文件的第一个版本,这个 blob 引用了 05408这个 blob,即该文件的第二个版本。命令输出内容的第三列显示的是对象大小,可以看到05408占用了 12K 空间,而 9bc1d 仅为 7 字节。非常有趣的是第二个版本才是完整保存文件内容的对象,而第一个版本是以差异方式保存的 ── 这是因为大部分情况下需要快速访问文件的最新版本。

最妙的是可以随时进行重新打包。Git 自动定期对仓库进行重新打包以节省空间。当然也可以手工运行 git gc 命令来这么做。

9.5  The Refspec

这本书读到这里,你已经使用过一些简单的远程分支到本地引用的映射方式了,这种映射可以更为复杂。 假设你像这样添加了一项远程仓库:


1

$
git remote add origin [email protected] github.com:schacon/simplegit-progit.git

它在你的 .git/config 文件中添加了一节,指定了远程的名称 (origin),
远程仓库的URL地址,和用于获取操作的 Refspec:


1

2

3

[remote"origin"]

       url
= [email protected] github.com:schacon/simplegit-progit.git

       fetch
= +refs/heads/*:refs/remotes/origin/*

Refspec 的格式是一个可选的 + 号,接着是 的格式,这里 是远端上的引用格式, 是将要记录在本地的引用格式。可选的 + 号告诉
Git 在即使不能快速演进的情况下,也去强制更新它。

缺省情况下 refspec 会被 git remote add 命令所自动生成, Git 会获取远端上 refs/heads/ 下面的所有引用,并将它写入到本地的refs/remotes/origin/.
所以,如果远端上有一个 master 分支,你在本地可以通过下面这种方式来访问它的历史记录:


1

2

3

$
git log origin/master

$
git log remotes/origin/master

$
git log refs/remotes/origin/master

它们全是等价的,因为 Git 把它们都扩展成 refs/remotes/origin/master.

如果你想让 Git 每次只拉取远程的 master 分支,而不是远程的所有分支,你可以把 fetch 这一行修改成这样:


1

fetch
= +refs/heads/master:refs/remotes/origin/master

这是 git fetch 操作对这个远端的缺省 refspec 值。而如果你只想做一次该操作,也可以在命令行上指定这个 refspec. 如可以这样拉取远程的master 分支到本地的 origin/mymaster 分支:


1

$
git fetch origin master:refs/remotes/origin/mymaster

你也可以在命令行上指定多个 refspec. 像这样可以一次获取远程的多个分支:


1

2

3

4

5

$
git fetch origin master:refs/remotes/origin/mymaster \

   topic:refs/remotes/origin/topic

From
[email protected] github.com:schacon/simplegit

 !
[rejected]        master     -> origin/mymaster  (non fast forward)

 *
[
newbranch]     
topic      -> origin/topic

在这个例子中, master 分支因为不是一个可以快速演进的引用而拉取操作被拒绝。你可以在 refspec 之前使用一个 + 号来重载这种行为。

你也可以在配置文件中指定多个 refspec. 如你想在每次获取时都获取 master 和 experiment 分支,就添加两行:


1

2

3

4

[remote"origin"]

       url
= [email protected] github.com:schacon/simplegit-progit.git

       fetch
= +refs/heads/master:refs/remotes/origin/master

       fetch
= +refs/heads/experiment:refs/remotes/origin/experiment

但是这里不能使用部分通配符,像这样就是不合法的:


1

fetch
= +refs/heads/qa*:refs/remotes/origin/qa*

但无论如何,你可以使用命名空间来达到这个目的。如你有一个QA组,他们推送一系列分支,你想每次获取 master 分支和QA组的所有分支,你可以使用这样的配置段落:


1

2

3

4

[remote"origin"]

       url
= [email protected] github.com:schacon/simplegit-progit.git

       fetch
= +refs/heads/master:refs/remotes/origin/master

       fetch
= +refs/heads/qa/*:refs/remotes/origin/qa/*

如果你的工作流很复杂,有QA组推送的分支、开发人员推送的分支、和集成人员推送的分支,并且他们在远程分支上协作,你可以采用这种方式为他们创建各自的命名空间。

推送 Refspec

采用命名空间的方式确实很棒,但QA组成员第1次是如何将他们的分支推送到 qa/ 空间里面的呢?答案是你可以使用 refspec 来推送。

如果QA组成员想把他们的 master 分支推送到远程的 qa/master 分支上,可以这样运行:


1

$
git push origin master:refs/heads/qa/master

如果他们想让 Git 每次运行 git push origin 时都这样自动推送,他们可以在配置文件中添加 push 值:


1

2

3

4

[remote"origin"]

       url
= [email protected] github.com:schacon/simplegit-progit.git

       fetch
= +refs/heads/*:refs/remotes/origin/*

       push
= refs/heads/master:refs/heads/qa/master

这样,就会让 git push origin 缺省就把本地的 master 分支推送到远程的 qa/master 分支上。

删除引用

你也可以使用 refspec 来删除远程的引用,是通过运行这样的命令:


1

$
git push origin :topic

因为 refspec 的格式是 , 通过把 部分留空的方式,这个意思是是把远程的topic 分支变成空,也就是删除它。

9.6  传输协议

Git 可以以两种主要的方式跨越两个仓库传输数据:基于HTTP协议之上,和 file://ssh://,
git:// 等智能传输协议。这一节带你快速浏览这两种主要的协议操作过程。

哑协议

Git 基于HTTP之上传输通常被称为哑协议,这是因为它在服务端不需要有针对 Git 特有的代码。这个获取过程仅仅是一系列GET请求,客户端可以假定服务端的Git仓库中的布局。让我们以 simplegit 库来看看http-fetch 的过程:


1

$
git clone http:
//github.com/schacon/simplegit-progit.git

它做的第1件事情就是获取 info/refs 文件。这个文件是在服务端运行了 update-server-info 所生成的,这也解释了为什么在服务端要想使用HTTP传输,必须要开启post-receive 钩子:



1

2

=>
GET info/refs

ca82a6dff817ec66f44342007202690a93763949    
refs/heads/master


现在你有一个远端引用和SHA值的列表。下一步是寻找HEAD引用,这样你就知道了在完成后,什么应该被检出到工作目录:



1

2

=>
GET HEAD

ref:
refs/heads/master


这说明在完成获取后,需要检出 master 分支。 这时,已经可以开始漫游操作了。因为你的起点是在 info/refs 文件中所提到的ca82a6 commit
对象,你的开始操作就是获取它:



1

2

=>
GET objects/ca/82a6dff817ec66f44342007202690a93763949

(179bytes
of binary data)


然后你取回了这个对象 - 这在服务端是一个松散格式的对象,你使用的是静态的 HTTP GET 请求获取的。可以使用 zlib 解压缩它,去除其头部,查看它的 commmit 内容:


1

2

3

4

5

6

7

8

9

10

11

$
git cat-file -p ca82a6dff817ec66f44342007202690a93763949

tree
cfda3bf379e4f8dba8717dee55aab78aef7f4daf

parent
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7

author
Scott Chacon

      1205815931-0700

committer
Scott Chacon

       1240030591-0700

changed
the version number

这样,就得到了两个需要进一步获取的对象 - cfda3b 是这个 commit 对象所对应的 tree 对象,和 085bb3 是它的父对象;



1

2

=>
GET objects/
08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7

(179bytes
of data)


这样就取得了这它的下一步 commit 对象,再抓取 tree 对象:



1

2

=>
GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf

(404-
Not Found)


Oops – 看起来这个 tree 对象在服务端并不以松散格式对象存在,所以得到了404响应,代表在HTTP服务端没有找到该对象。这有好几个原因 - 这个对象可能在替代仓库里面,或者在打包文件里面, Git 会首先检查任何列出的替代仓库:



1

2

=>
GET objects/info/http-alternates

(empty
file)


如果这返回了几个替代仓库列表,那么它会去那些地方检查松散格式对象和文件 - 这是一种在软件分叉之间共享对象以节省磁盘的好方法。然而,在这个例子中,没有替代仓库。所以你所需要的对象肯定在某个打包文件中。要检查服务端有哪些打包格式文件,你需要获取objects/info/packs 文件,这里面包含有打包文件列表(是的,它也是被 update-server-info 所生成的);



1

2

=>
GET objects/info/packs

P
pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack


这里服务端只有一个打包文件,所以你要的对象显然就在里面。但是你可以先检查它的索引文件以确认。这在服务端有多个打包文件时也很有用,因为这样就可以先检查你所需要的对象空间是在哪一个打包文件里面了:



1

2

=>
GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx

(4k
of binary data)


现在你有了这个打包文件的索引,你可以看看你要的对象是否在里面 - 因为索引文件列出了这个打包文件所包含的所有对象的SHA值,和该对象存在于打包文件中的偏移量,所以你只需要简单地获取整个打包文件:



1

2

=>
GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack

(13k
of binary data)


现在你也有了这个 tree 对象,你可以继续在 commit 对象上漫游。它们全部都在这个你已经下载到的打包文件里面,所以你不用继续向服务端请求更多下载了。 在这完成之后,由于下载开始时已探明HEAD引用是指向master 分支, Git 会将它检出到工作目录。

整个过程看起来就像这样:


1

2

3

4

5

6

7

8

9

10

11

12

$
git clone http:
//github.com/schacon/simplegit-progit.git

Initialized
empty Git repository
in/private/tmp/simplegit-progit/.git/

got
ca82a6dff817ec66f44342007202690a93763949

walk
ca82a6dff817ec66f44342007202690a93763949

got
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7

Getting
alternates list
forhttp://github.com/schacon/simplegit-progit.git

Getting
pack list
forhttp://github.com/schacon/simplegit-progit.git

Getting
index
forpack
816a9b2334da9953e530f27bcac22082a9f5b835

Getting
pack 816a9b2334da9953e530f27bcac22082a9f5b835

 which
contains cfda3bf379e4f8dba8717dee55aab78aef7f4daf

walk
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7

walk
a11bef06a3f659402fe7563abf99ad00de2209e6

智能协议

这个HTTP方法是很简单但效率不是很高。使用智能协议是传送数据的更常用的方法。这些协议在远端都有Git智能型进程在服务 - 它可以读出本地数据并计算出客户端所需要的,并生成合适的数据给它,这有两类传输数据的进程:一对用于上传数据和一对用于下载。

上传数据

为了上传数据至远端, Git 使用 send-pack 和 receive-pack 进程。这个 send-pack 进程运行在客户端上,它连接至远端运行的 receive-pack 进程。

举例来说,你在你的项目上运行了 git push origin master, 并且 origin 被定义为一个使用SSH协议的URL。
Git 会使用send-pack 进程,它会启动一个基于SSH的连接到服务器。它尝试像这样透过SSH在服务端运行命令:


1

2

3

4

$
ssh -x [email protected] github.com
"git-receive-pack
‘schacon/simplegit-progit.git‘"

005bca82a6dff817ec66f4437202690a93763949
refs/heads/master report-status
delete-refs

003e085bb3bcb608e1e84b2432f8ecbe6306e7e7
refs/heads/topic

0000

这里的 git-receive-pack 命令会立即对它所拥有的每一个引用响应一行 - 在这个例子中,只有 master 分支和它的SHA值。这里第1行也包含了服务端的能力列表(这里是report-status 和 delete-refs)。

每一行以4字节的十六进制开始,用于指定整行的长度。你看到第1行以005b开始,这在十六进制中表示91,意味着第1行有91字节长。下一行以003e起始,表示有62字节长,所以需要读剩下的62字节。再下一行是0000开始,表示服务器已完成了引用列表过程。

现在它知道了服务端的状态,你的 send-pack 进程会判断哪些 commit 是它所拥有但服务端没有的。针对每个引用,这次推送都会告诉对端的receive-pack 这个信息。举例说,如果你在更新 master 分支,并且增加 experiment 分支,这个send-pack 将会是像这样:


1

2

3

0085ca82a6dff817ec66f44342007202690a93763949 
15027957951b64cf874c3557a0f3547bd83b3ff6 refs/heads/master report-status

00670000000000000000000000000000000000000000cdfdb42577e2506715f8cfeacdbabc092bf63e8d
refs/heads/experiment

0000

这里的全’0’的SHA-1值表示之前没有过这个对象 - 因为你是在添加新的 experiment 引用。如果你在删除一个引用,你会看到相反的: 就是右边是全’0’。

Git 针对每个引用发送这样一行信息,就是旧的SHA值,新的SHA值,和将要更新的引用的名称。第1行还会包含有客户端的能力。下一步,客户端会发送一个所有那些服务端所没有的对象的一个打包文件。最后,服务端以成功(或者失败)来响应:


1

000Aunpack
ok

下载数据

当你在下载数据时, fetch-pack 和 upload-pack 进程就起作用了。客户端启动 fetch-pack 进程,连接至远端的 upload-pack 进程,以协商后续数据传输过程。

在远端仓库有不同的方式启动 upload-pack 进程。你可以使用与 receive-pack 相同的透过SSH管道的方式,也可以通过
Git 后台来启动这个进程,它默认监听在9418号端口上。这里fetch-pack 进程在连接后像这样向后台发送数据:


1

003fgit-upload-pack
schacon/simplegit-progit.git\0host=myserver.com\
0

它也是以4字节指定后续字节长度的方式开始,然后是要运行的命令,和一个空字节,然后是服务端的主机名,再跟随一个最后的空字节。 Git 后台进程会检查这个命令是否可以运行,以及那个仓库是否存在,以及是否具有公开权限。如果所有检查都通过了,它会启动这个upload-pack 进程并将客户端的请求移交给它。

如果你透过SSH使用获取功能, fetch-pack 会像这样运行:


1

$
ssh -x [email protected] github.com
"git-upload-pack
‘schacon/simplegit-progit.git‘"

不管哪种方式,在 fetch-pack 连接之后, upload-pack 都会以这种形式返回:


1

2

3

4

5

0088ca82a6dff817ec66f44342007202690a93763949
HEAD\0multi_ack thin-pack \

  side-band
side-band-64k ofs-delta shallow no-progress
include-tag

003fca82a6dff817ec66f44342007202690a93763949
refs/heads/master

003e085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
refs/heads/topic

0000

这与 receive-pack 响应很类似,但是这里指的能力是不同的。而且它还会指出HEAD引用,让客户端可以检查是否是一份克隆。

在这里, fetch-pack 进程检查它自己所拥有的对象和所有它需要的对象,通过发送 “want” 和所需对象的SHA值,发送 “have” 和所有它已拥有的对象的SHA值。在列表完成时,再发送 “done” 通知upload-pack 进程开始发送所需对象的打包文件。这个过程看起来像这样:


1

2

3

4

0054want
ca82a6dff817ec66f44342007202690a93763949 ofs-delta

0032have
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7

0000

0009done

这是传输协议的一个很基础的例子,在更复杂的例子中,客户端可能会支持 multi_ack 或者 side-band 能力;但是这个例子中展示了智能协议的基本交互过程。

9.7  维护及数据恢复

你时不时的需要进行一些清理工作 ── 如减小一个仓库的大小,清理导入的库,或是恢复丢失的数据。本节将描述这类使用场景。

维护

Git 会不定时地自动运行称为 “auto gc” 的命令。大部分情况下该命令什么都不处理。不过要是存在太多松散对象 (loose object, 不在 packfile 中的对象) 或 packfile,Git 会进行调用git gc 命令。 gc 指垃圾收集
(garbage collect),此命令会做很多工作:收集所有松散对象并将它们存入 packfile,合并这些 packfile 进一个大的 packfile,然后将不被任何 commit 引用并且已存在一段时间 (数月) 的对象删除。

可以手工运行 auto gc 命令:


1

$
git gc --auto

再次强调,这个命令一般什么都不干。如果有 7,000 个左右的松散对象或是 50 个以上的 packfile,Git 才会真正调用 gc 命令。可能通过修改配置中的gc.auto 和 gc.autopacklimit 来调整这两个阈值。

gc 还会将所有引用 (references) 并入一个单独文件。假设仓库中包含以下分支和标签:


1

2

3

4

5

$
find .git/refs -type f

.git/refs/heads/experiment

.git/refs/heads/master

.git/refs/tags/v1.0

.git/refs/tags/v1.1

这时如果运行 git gcrefs 下的所有文件都会消失。Git
会将这些文件挪到 .git/packed-refs 文件中去以提高效率,该文件是这个样子的:


1

2

3

4

5

6

7

$
cat .git/packed-refs

#
pack-refs
with:
peeled

cac0cab538b970a37ea1e769cbbde608743bc96d
refs/heads/experiment

ab1afef80fac8e34258ff41fc1b867c702daa24b
refs/heads/master

cac0cab538b970a37ea1e769cbbde608743bc96d
refs/tags/v1.
0

9585191f37f7b0fb9444f35a9bf50de191beadc2
refs/tags/v1.
1

^1a410efbd13591db07496601ebc7a059dd55cfe9

当更新一个引用时,Git 不会修改这个文件,而是在 refs/heads 下写入一个新文件。当查找一个引用的 SHA 时,Git 首先在refs 目录下查找,如果未找到则到 packed-refs 文件中去查找。因此如果在 refs 目录下找不到一个引用,该引用可能存到packed-refs 文件中去了。

请留意文件最后以 ^ 开头的那一行。这表示该行上一行的那个标签是一个 annotated 标签,而该行正是那个标签所指向的 commit 。

数据恢复

在使用 Git 的过程中,有时会不小心丢失 commit 信息。这一般出现在以下情况下:强制删除了一个分支而后又想重新使用这个分支,hard-reset 了一个分支从而丢弃了分支的部分 commit。如果这真的发生了,有什么办法把丢失的 commit 找回来呢?

下面的示例演示了对 test 仓库主分支进行 hard-reset 到一个老版本的 commit 的操作,然后恢复丢失的 commit 。首先查看一下当前的仓库状态:


1

2

3

4

5

6

$
git log --pretty=oneline

ab1afef80fac8e34258ff41fc1b867c702daa24b
modified repo a bit

484a59275031909e19aadb7c92262719cfcdf19a
added repo.rb

1a410efbd13591db07496601ebc7a059dd55cfe9
third commit

cac0cab538b970a37ea1e769cbbde608743bc96d
second commit

fdf4fc3344e67ab068f836878b6c4951e3b15f3d
first commit

接着将 master 分支移回至中间的一个 commit:


1

2

3

4

5

6

$
git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9

HEADisnow
at 1a410ef third commit

$
git log --pretty=oneline

1a410efbd13591db07496601ebc7a059dd55cfe9
third commit

cac0cab538b970a37ea1e769cbbde608743bc96d
second commit

fdf4fc3344e67ab068f836878b6c4951e3b15f3d
first commit

这样就丢弃了最新的两个 commit ── 包含这两个 commit 的分支不存在了。现在要做的是找出最新的那个 commit 的 SHA,然后添加一个指它它的分支。关键在于找出最新的 commit 的 SHA ── 你不大可能记住了这个 SHA,是吧?

通常最快捷的办法是使用 git reflog 工具。当你 (在一个仓库下) 工作时,Git 会在你每次修改了 HEAD 时悄悄地将改动记录下来。当你提交或修改分支时,reflog 就会更新。git
update-ref
 命令也可以更新 reflog,这是在本章前面的 “Git References” 部分我们使用该命令而不是手工将 SHA 值写入 ref 文件的理由。任何时间运行git reflog 命令可以查看当前的状态:


1

2

3

$
git reflog

1a410ef
[email protected]{
0}:
1a410efbd13591db07496601ebc7a059dd55cfe9: updating HEAD

ab1afef
[email protected]{
1}:
ab1afef80fac8e34258ff41fc1b867c702daa24b: updating HEAD

可以看到我们签出的两个 commit ,但没有更多的相关信息。运行 git log -g 会输出 reflog 的正常日志,从而显示更多有用信息:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

$
git log -g

commit
1a410efbd13591db07496601ebc7a059dd55cfe9

Reflog:
[email protected]{
0}
(Scott Chacon

     )

Reflog
message: updating HEAD

Author:
Scott Chacon

Date:  
Fri May
2218:22:372009

-
0700

    third
commit

commit
ab1afef80fac8e34258ff41fc1b867c702daa24b

Reflog:
[email protected]{
1}
(Scott Chacon

       )

Reflog
message: updating HEAD

Author:
Scott Chacon

Date:  
Fri May
2218:15:242009

-
0700

     modified
repo a bit

看起来弄丢了的 commit 是底下那个,这样在那个 commit 上创建一个新分支就能把它恢复过来。比方说,可以在那个 commit (ab1afef) 上创建一个名为recover-branch 的分支:


1

2

3

4

5

6

7

$
git branch recover-branch ab1afef

$
git log --pretty=oneline recover-branch

ab1afef80fac8e34258ff41fc1b867c702daa24b
modified repo a bit

484a59275031909e19aadb7c92262719cfcdf19a
added repo.rb

1a410efbd13591db07496601ebc7a059dd55cfe9
third commit

cac0cab538b970a37ea1e769cbbde608743bc96d
second commit

fdf4fc3344e67ab068f836878b6c4951e3b15f3d
first commit

酷!这样有了一个跟原来 master 一样的 recover-branch 分支,最新的两个
commit 又找回来了。接着,假设引起 commit 丢失的原因并没有记录在 reflog 中 ── 可以通过删除recover-branch 和 reflog 来模拟这种情况。这样最新的两个 commit 不会被任何东西引用到:



1

2

$
git branch -D recover-branch

$
rm -Rf .git/logs/


因为 reflog 数据是保存在 .git/logs/ 目录下的,这样就没有 reflog 了。现在要怎样恢复 commit 呢?办法之一是使用git
fsck
 工具,该工具会检查仓库的数据完整性。如果指定 --ful 选项,该命令显示所有未被其他对象引用 (指向) 的所有对象:


1

2

3

4

5

$
git fsck --full

dangling
blob d670460b4b4aece5915caf5c68d12f560a9fe3e4

dangling
commit ab1afef80fac8e34258ff41fc1b867c702daa24b

dangling
tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9

dangling
blob 7108f7ecb345ee9d0084193f147cdad4d2998293

本例中,可以从 dangling commit 找到丢失了的 commit。用相同的方法就可以恢复它,即创建一个指向该 SHA 的分支。

移除对象

Git 有许多过人之处,不过有一个功能有时却会带来问题:git clone 会将包含每一个文件的所有历史版本的整个项目下载下来。如果项目包含的仅仅是源代码的话这并没有什么坏处,毕竟 Git 可以非常高效地压缩此类数据。不过如果有人在某个时刻往项目中添加了一个非常大的文件,那们即便他在后来的提交中将此文件删掉了,所有的签出都会下载这个
大文件。因为历史记录中引用了这个文件,它会一直存在着。

当你将 Subversion 或 Perforce 仓库转换导入至 Git 时这会成为一个很严重的问题。在此类系统中,(签出时) 不会下载整个仓库历史,所以这种情形不大会有不良后果。如果你从其他系统导入了一个仓库,或是发觉一个仓库的尺寸远超出预计,可以用下面的方法找到并移除 大 (尺寸) 对象。

警告:此方法会破坏提交历史。为了移除对一个大文件的引用,从最早包含该引用的 tree 对象开始之后的所有 commit 对象都会被重写。如果在刚导入一个仓库并在其他人在此基础上开始工作之前这么做,那没有什么问题 ── 否则你不得不通知所有协作者 (贡献者) 去衍合你新修改的 commit 。

为了演示这点,往 test 仓库中加入一个大文件,然后在下次提交时将它删除,接着找到并将这个文件从仓库中永久删除。首先,加一个大文件进去:


1

2

3

4

5

6

$
curl http:
//kernel.org/pub/software/scm/git/git-1.6.3.1.tar.bz2
> git.tbz2

$
git add git.tbz2

$
git commit -am
‘added
git tarball‘

[master
6df7640] added git tarball

 1files
changed,
0insertions(+),
0deletions(-)

 create
mode
100644git.tbz2

喔,你并不想往项目中加进一个这么大的 tar 包。最后还是去掉它:


1

2

3

4

5

6

$
git rm git.tbz2

rm‘git.tbz2‘

$
git commit -m
‘oops
- removed large tarball‘

[master
da3f30d] oops - removed large tarball

 1files
changed,
0insertions(+),
0deletions(-)

 deletemode
100644git.tbz2

对仓库进行 gc 操作,并查看占用了空间:


1

2

3

4

5

6

$
git gc

Counting
objects:
21,
done.

Delta
compression using
2threads.

Compressing
objects:
100%
(
16/16),
done.

Writing
objects:
100%
(
21/21),
done.

Total21(delta
3),
reused
15(delta
1)

可以运行 count-objects 以查看使用了多少空间:


1

2

3

4

5

6

7

8

$
git count-objects -v

count:4

size:16

in-pack:21

packs:1

size-pack:2016

prune-packable:0

garbage:0

size-pack 是以千字节为单位表示的 packfiles 的大小,因此已经使用了 2MB 。而在这次提交之前仅用了 2K 左右 ── 显然在这次提交时删除文件并没有真正将其从历史记录中删除。每当有人复制这个仓库去取得这个小项目时,都不得不复制所有 2MB
数据,而这仅仅因为你曾经不小心加了个大文件。当我们来解决这个问题。

首先要找出这个文件。在本例中,你知道是哪个文件。假设你并不知道这一点,要如何找出哪个 (些) 文件占用了这么多的空间?如果运行 git gc,所有对象会存入一个 packfile 文件;运行另一个底层命令git
verify-pack
 以识别出大对象,对输出的第三列信息即文件大小进行排序,还可以将输出定向到 tail 命令,因为你只关心排在最后的那几个最大的文件:


1

2

3

4

$
git verify-pack -v .git/objects/pack/pack-3f8c0...bb.idx | sort -k
3-n
| tail -
3

e3f094f522629ae358806b17daf78246c27c007b
blob  
1486734

4667

05408d195263d853f09dca71d55116663690c27c
blob  
129083478

1189

7a9eb2fba2b1811321254ac360970fc169ba2330
blob  
20567162056872

5401

最底下那个就是那个大文件:2MB 。要查看这到底是哪个文件,可以使用第 7 章中已经简单使用过的 rev-list 命令。若给 rev-list 命令传入 --objects 选项,它会列出所有
commit SHA 值,blob SHA 值及相应的文件路径。可以这样查看 blob 的文件名:


1

2

$
git rev-list --objects --all | grep 7a9eb2fb

7a9eb2fba2b1811321254ac360970fc169ba2330
git.tbz2

接下来要将该文件从历史记录的所有 tree 中移除。很容易找出哪些 commit 修改了这个文件:


1

2

3

$
git log --pretty=oneline -- git.tbz2

da3f30d019005479c99eb4c3406225613985a1db
oops - removed large tarball

6df764092f3e7c8f5f94cbe08ee5cf42e92a0289
added git tarball

必须重写从 6df76 开始的所有 commit 才能将文件从 Git 历史中完全移除。这么做需要用到
6 章
中用过的 filter-branch 命令:


1

2

3

4

5

$
git filter-branch --index-filter \

   ‘git
rm --cached --ignore-unmatch git.tbz2‘

-- 6df7640^..

Rewrite
6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 (
1/2)rm‘git.tbz2‘

Rewrite
da3f30d019005479c99eb4c3406225613985a1db (
2/2)

Ref‘refs/heads/master‘was
rewritten

--index-filter 选项类似于
6 章
中使用的 --tree-filter 选项,但这里不是传入一个命令去修改磁盘上签出的文件,而是修改暂存区域或索引。不能用rm
file
 命令来删除一个特定文件,而是必须用 git rm --cached 来删除它 ── 即从索引而不是磁盘删除它。这样做是出于速度考虑 ── 由于 Git 在运行你的 filter 之前无需将所有版本签出到磁盘上,这个操作会快得多。也可以用--tree-filter 来完成相同的操作。git
rm
 的 --ignore-unmatch 选项指定当你试图删除的内容并不存在时不显示错误。最后,因为你清楚问题是从哪个 commit 开始的,使用filter-branch 重写自 6df7640 这个
commit 开始的所有历史记录。不这么做的话会重写所有历史记录,花费不必要的更多时间。

现在历史记录中已经不包含对那个文件的引用了。不过 reflog 以及运行 filter-branch 时 Git 往 .git/refs/original 添加的一些
refs 中仍有对它的引用,因此需要将这些引用删除并对仓库进行 repack 操作。在进行 repack 前需要将所有对这些 commits 的引用去除:


1

2

3

4

5

6

7

8

$
rm -Rf .git/refs/original

$
rm -Rf .git/logs/

$
git gc

Counting
objects:
19,
done.

Delta
compression using
2

threads.

Compressing
objects:
100%
(
14/14),
done.

Writing
objects:
100%
(
19/19),
done.

Total
19

(delta
3),
reused
16

(delta
1)

看一下节省了多少空间。


1

2

3

4

5

6

7

8

$
git count-objects -v

count:
8

size:
2040

in-pack:
19

packs:
1

size-pack:
7

prune-packable:
0

garbage:
0

repack 后仓库的大小减小到了 7K ,远小于之前的 2MB 。从 size 值可以看出大文件对象还在松散对象中,其实并没有消失,不过这没有关系,重要的是在再进行推送或复制,这个对象不会再传送出去。如果真的要完全把这个对象删除,可以运行git prune --expire 命令。

9.8  总结

现在你应该对 Git 可以做什么相当了解了,并且在一定程度上也知道了 Git 是如何实现的。本章覆盖了许多 plumbing 命令 ── 这些命令比较底层,且比你在本书其他部分学到的 porcelain 命令要来得简单。从底层了解 Git 的工作原理可以帮助你更好地理解为何 Git 实现了目前的这些功能,也使你能够针对你的工作流写出自己的工具和脚本。

Git 作为一套 content-addressable 的文件系统,是一个非常强大的工具,而不仅仅只是一个 VCS 供人使用。希望借助于你新学到的 Git 内部原理的知识,你可以实现自己的有趣的应用,并以更高级便利的方式使用 Git。

时间: 2024-08-16 01:41:04

Git专题--系统的学习Git之四的相关文章

Git专题--系统的学习Git之一

本文为整理自:伯乐在线 1.Git详解之一:Git起步 2.Git详解之二:Git基础 3.Git详解之三:Git分支 4.Git详解之四:服务器上的Git 5.Git详解之五:分布式Git 6.Git详解之六:Git工具 7.Git详解之七:自定义Git 8.Git详解之八:Git与其他系统 9.Git详解之八:Git与其他系统 Git详解之一:Git起步 起步 本章介绍开始使用 Git 前的相关知识.我们会先了解一些版本控制工具的历史背景,然后试着让 Git 在你的系统上跑起来,直到最后配置

Git专题--系统的学习Git之二

本文为整理自:伯乐在线 1.Git详解之一:Git起步 2.Git详解之二:Git基础 3.Git详解之三:Git分支 4.Git详解之四:服务器上的Git 5.Git详解之五:分布式Git 6.Git详解之六:Git工具 7.Git详解之七:自定义Git 8.Git详解之八:Git与其他系统 9.Git详解之九:Git内部原理 Git详解之四:服务器上的Git 服务器上的 Git 到目前为止,你应该已经学会了使用 Git 来完成日常工作.然而,如果想与他人合作,还需要一个远程的 Git 仓库.

Git专题--系统的学习Git之三

本文为整理自:伯乐在线 1.Git详解之一:Git起步 2.Git详解之二:Git基础 3.Git详解之三:Git分支 4.Git详解之四:服务器上的Git 5.Git详解之五:分布式Git 6.Git详解之六:Git工具 7.Git详解之七:自定义Git 8.Git详解之八:Git与其他系统 9.Git详解之九:Git内部原理 Git详解之六:Git工具 Git 工具 现在,你已经学习了管理或者维护 Git 仓库,实现代码控制所需的大多数日常命令和工作流程.你已经完成了跟踪和提交文件的基本任务

git学习——Git 基础要点【转】

转自:http://blog.csdn.net/zeroboundary/article/details/10549555 简单地说,Git 究竟是怎样的一个系统呢?请注意,接下来的内容非常重要,若是理解了 Git 的思想和基本的工作原理,用起来就会知其所以然,游刃有余.在开始学习 Git 的时候,请不要尝试把各种概念和其他的版本控制系统诸如 Subversion 和 Perforce 等相比拟,否则容易混淆每个操作的实际意义.Git 在保存和处理各种信息的时候,虽然操作起来的命令形式非常相近,

git学习——git命令之创建版本库

原文来至 一.创建版本库 版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改.删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以"还原". 所以,创建一个版本库非常简单,首先,选择一个合适的地方,创建一个空目录: $ mkdir learngit $ cd learngit $ pwd /Users/michael/learngit pwd命令用于显示当前目录.在我的Mac上,这个仓库

《Git Community Book》学习笔记

打算全面的学习一下Git的相关知识,比较网上的资料,觉得<Git社区书>篇幅合适,覆盖全面,就是它了.chapter1 介绍 1.Git是一个快速的分布式版本控制系统.2.所有用来表示项目历史信息的文件,是通过一个40个字符的(40-digit)“ 对象名” 来索引的.每一个“ 对象名” 都是对“ 对象” 内容做SHA1哈希计算得来的,(SHA1是一种密码学的哈希算法).3.与SVN的区别Git与你熟悉的大部分版本控制系统的差别是很大的.也许你熟悉Subversion.CVS.Perforce

[转]深入理解学习GIT工作流

深入理解学习Git工作流 字数13437 阅读2761 评论3 喜欢70 个人在学习git工作流的过程中,从原有的 SVN 模式很难完全理解git的协作模式,直到有一天我看到了下面的文章,好多遗留在心中的困惑迎刃而解,于是我将这部分资料进行整理放到了github上,欢迎star查看最新更新内容, https://github.com/xirong/my-git/blob/master/git-workflow-tutorial.md 我们以使用SVN的工作流来使用git有什么不妥? git 方便

git和github的学习

第一部分:我的github地址 https://github.com/AllOVERQ/first/tree/master 第二部分:git和github Git是一款免费.开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目.Git是一个开源的分布式版本控制系统,可以有效.高速的处理从很小到非常大的项目版本管理.Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件.分布式相比于集中式的最大区别在于开发者可以提交到本地,每个开发

Git学习——Git分支篇(未完)

Git学习--Git分支篇(未完) 前言 完成了Git学习的基础篇,继续学习Git的分支特性,这是Git出众之处. 目录 分支简介 分支创建 分支切换 分支新建与合并 分支新建 分支合并 遇到冲突时的分支合并 分支简介 首先,Git保存数据的方式比较特殊,保存的是文件的快照,而不是文件的变化. 因此,在执行提交( commit )操作时,Git会保存一个提交对象( commit object).该提交对象包含一个指针指向暂存的内容快照,同时包含作者的姓名.邮箱.提交时输入的信息和指向它父对象的指