4.2 思考:为什么工作区根目录下有一个.git目录
Git及其他分布式版本控制系统(如Mercurial/Hg、Bazaar)的一个共同的显著特点是,版本库位于工作区的根目录下。对于Git来说,版本库位于工作区根目录下的.git目录中,且仅此一处,在工作区的子目录下则没有任何其他跟踪文件或目录。Git的这种设计要比CVS和Subversion等传统的集中式版本控制工具方便多了。
传统的集中式版本控制系统的版本库和工作区是分开的,甚至是在不同的主机上,因此必须建立工作区和版本库的对应。下面来看看版本控制系统的前辈们是如何建立工作区和版本库的跟踪的,通过其各自设计的优缺点,我们会更深刻地体会到Git实现的必要和巧妙。
对于CVS而言,工作区的根目录及每一个子目录下都有一个CVS目录,CVS目录中包含几个配置文件,建立了对版本库的追踪。如CVS目录下的Entries文件记录了从版本库检出到工作区的文件的名称、版本和时间戳等,通过时间戳的对比可快速扫描工作区文件的改动。这样设计的好处是,可以将工作区移动到任何其他目录中,而工作区和版本控制服务器的映射关系保持不变,这样工作区依然能够正常工作。甚至还可以将工作区的某个子目录移动到其他位置,形成新的工作区,在新的工作区下仍然可以完成版本控制相关的操作。但是缺点也很多,例如,如果工作区文件修改了,因为没有原始文件做比对,所以向服务器提交修改时只能对整个文件进行传输,而不能仅传输文件的改动部分,导致从客户端到服务器的网络传输效率降低。还有一个风险是信息泄漏,例如,Web服务器的目录下如果包含了CVS目录,黑客就可以通过扫描CVS/Entries文件得到目录下的文件列表,从而获得他们想要的信息。
对于Subversion来说,工作区的根目录和每一个子目录下都有一个.svn目录。目录.svn中不仅包含了类似于CVS的跟踪目录下的配置文件,而且包含了当前工作区下每一个文件的拷贝。这些文件的原始拷贝让某些SVN子命令可以脱离版本库执行。而且,当由客户端向服务器提交时,可以只提交改动的部分,因为改动的文件可以与文件的原始拷贝进行差异比较。但是,这么做也有缺点,除了会像CVS那样因为引入CVS跟踪目录而有可能造成信息泄漏外,还会加倍占用工作区的空间。此外,当在工作区目录下针对文件内容进行搜索时,会因为.svn目录下文件的原始拷贝导致搜索结果加倍,使搜索结果混乱。
有的版本控制系统在工作区根本就没有任何跟踪文件,例如,某款商业的版本控制软件(就不点名了)的工作区就非常干净,没有任何配置文件和配置目录。但是,这样的设计更糟糕,因为它实际上是通过服务器端建立文件跟踪,在服务器端的数据库中保存了一个包含如下信息的表格:哪个客户端,在哪个本地目录检出了哪个版本的版本库文件。这样做的后果是,如果客户端将工作区移动或改名,就会导致文件的跟踪状态丢失,从而出现文件状态未知的问题。此外,客户端操作系统重装也会导致文件跟踪状态丢失。
Git这种将版本库放在工作区根目录下的设计使得所有的版本控制操作(除了与其他远程版本库之间的互操作)都在本地即可完成,不像Subversion只有寥寥无几的几个命令脱离网络执行。而且,Git没有CVS和Subversion中存在的安全泄漏问题(只要保护好.git目录),也不会像Subversion那样在搜索本地文件时出现搜索结果混乱的问题。甚至,Git还提供了一条git grep命令来更好地搜索工作区的文件内容,例如,我们可以在本书的Git库中执行下面的命令来搜索版本库中的文件内容:
$git grep "工作区文件内容搜索"
02-git-solo/010-git-init.rst:'git grep'命令来更好地搜索工作区的文件内容。
Git将版本库(.git目录)放在工作区根目录下,那么Git的相关操作一定要在工作区根目录下执行吗?换句话说,当工作区中包含子目录,并在子目录中执行Git命令时,如何定位版本库呢?
实际上,当在Git工作区的某个子目录下执行操作的时候,会在工作区目录中依次向上递归查找.git目录,找到的.git目录就是工作区对应的版本库,.git所在的目录就是工作区的根目录,文件.git/index记录了工作区文件的状态(实际上是暂存区的状态)。
例如,在非Git工作区执行git命令时会因为找不到.git目录而报错。
$cd/path/to/my/workspace/
$git status
fatal:Not a git repository(or any of the parent directories):.git
如果用strace[1]命令去跟踪执行git status命令时的磁盘访问,会看到沿目录依次向上递归的过程。
$strace-e' trace=file' git status
…
getcwd("/path/to/my/workspace",4096)=14
stat(".",{st_mode=S_IFDIR|0755,st_size=4096,…})=0
stat(".git",0x7fffdf1288d0)=-1 ENOENT(No such file or directory)
access(".git/objects",X_OK)=-1 ENOENT(No such file or directory)
access("./objects",X_OK)=-1 ENOENT(No such file or directory)
stat("..",{st_mode=S_IFDIR|0755,st_size=4096,…})=0
chdir("..")=0
stat(".git",0x7fffdf1288d0)=-1 ENOENT(No such file or directory)
access(".git/objects",X_OK)=-1 ENOENT(No such file or directory)
access("./objects",X_OK)=-1 ENOENT(No such file or directory)
stat("..",{st_mode=S_IFDIR|0755,st_size=4096,…})=0
chdir("..")=0
stat(".git",0x7fffdf1288d0)=-1 ENOENT(No such file or directory)
access(".git/objects",X_OK)=-1 ENOENT(No such file or directory)
access("./objects",X_OK)=-1 ENOENT(No such file or directory)
fatal:Not a git repository(or any of the parent directories):.git
当在工作区执行Git命令时,上面查找版本库的操作总是默默地执行,就好像什么也没有发生一样。那么有什么办法知道Git版本库的位置呢?如何才能知道工作区的根目录在哪里呢?可以用Git的一个底层命令来实现,具体操作过程如下:
(1)在工作区中建立目录a/b/c,进入到该目录中。
$cd/path/to/my/workspace/demo/
$mkdir-p a/b/c
$cd/path/to/my/workspace/demo/a/b/c
(2)显示版本库.git目录所在的位置。
$git rev-parse—git-dir
/path/to/my/workspace/demo/.git
(3)显示工作区根目录。
$git rev-parse—show-toplevel
/path/to/my/workspace/demo
(4)相对于工作区根目录的相对目录。
$git rev-parse—show-prefix
a/b/c/
(5)显示从当前目录(cd)后退(up)到工作区的根的深度。
$git rev-parse—show-cdup
../../../
传统的集中式版本控制系统的工作区和版本库都是相分离的,像Git这样把版本库目录放在工作区是不是太不安全了?
从存储安全的角度上来讲,将版本库放在工作区目录下有点“把鸡蛋装在一个篮子里”的味道。如果忘记了工作区中还有版本库,当直接从工作区的根执行目录删除操作时就会连版本库一并删除,这个风险的确很高。将版本库和工作区拆开似乎更加安全,但是不要忘了之前的讨论,如果将版本库和工作区拆开,就要引入其他机制以便实现版本库对工作区的追踪。
Git克隆可以降低因为版本库和工作区混杂在一起而导致的版本库被破坏的风险。可以通过克隆操作在本机另外的磁盘/目录中建立Git克隆,并在工作区有新的提交时,手动或自动地执行向克隆版本库的推送(git push)操作。如果使用网络协议,还可以实现在其他机器上建立克隆,这样就更安全了(双机备份)。对于使用Git做版本控制的团队,每个人都是一个备份,因此团队开发中的Git版本库更安全,管理员甚至无须顾虑版本库存储的安全问题。
[1]Mac OS X可以使用dtruss命令。