配色: 字号:
Git详解之六Git工具
2017-03-31 | 阅:  转:  |  分享 
  
Git详解之六Git工具http://www.open-open.com/lib/view/open1328070367499.html单
个修订版本http://www.open-open.com/lib/view/open1328070367499.html简短的S
HAhttp://www.open-open.com/lib/view/open1328070367499.html关于SHA-
1的简短说明http://www.open-open.com/lib/view/open1328070367499.html分支
引用http://www.open-open.com/lib/view/open1328070367499.html引用日志里的简
称http://www.open-open.com/lib/view/open1328070367499.html祖先引用http
://www.open-open.com/lib/view/open1328070367499.html提交范围http://ww
w.open-open.com/lib/view/open1328070367499.html暂存和撤回文件http://www.
open-open.com/lib/view/open1328070367499.html暂存补丁http://www.open-
open.com/lib/view/open1328070367499.html储藏你的工作http://www.open-ope
n.com/lib/view/open1328070367499.htmlUn-applyingaStashhttp://ww
w.open-open.com/lib/view/open1328070367499.html从储藏中创建分支http://www
.open-open.com/lib/view/open1328070367499.html改变最近一次提交http://www.
open-open.com/lib/view/open1328070367499.html修改多个提交说明http://www.o
pen-open.com/lib/view/open1328070367499.html重排提交http://www.open-o
pen.com/lib/view/open1328070367499.html压制(Squashing)提交http://www.
open-open.com/lib/view/open1328070367499.html拆分提交http://www.open-
open.com/lib/view/open1328070367499.html核弹级选项:filter-branchhttp:
//www.open-open.com/lib/view/open1328070367499.html文件标注http://www
.open-open.com/lib/view/open1328070367499.html二分查找http://www.open
-open.com/lib/view/open1328070367499.html子模块初步http://www.open-ope
n.com/lib/view/open1328070367499.html克隆一个带子模块的项目http://www.open-o
pen.com/lib/view/open1328070367499.html上层项目http://www.open-open.c
om/lib/view/open1328070367499.html子模块的问题Git工具现在,你已经学习了管理或者维护Git
仓库,实现代码控制所需的大多数日常命令和工作流程。你已经完成了跟踪和提交文件的基本任务,并且发挥了暂存区和轻量级的特性分支及合并
的威力。接下来你将领略到一些Git可以实现的非常强大的功能,这些功能你可能并不会在日常操作中使用,但在某些时候你也许会需要。6
.1?修订版本(Revision)选择Git允许你通过几种方法来指明特定的或者一定范围内的提交。了解它们并不是必需的,但是了解
一下总没坏处。单个修订版本显然你可以使用给出的SHA-1值来指明一次提交,不过也有更加人性化的方法来做同样的事。本节概述了指明
单个提交的诸多方法。简短的SHAGit很聪明,它能够通过你提供的前几个字符来识别你想要的那次提交,只要你提供的那部分SHA-1
不短于四个字符,并且没有歧义——也就是说,当前仓库中只有一个对象以这段SHA-1开头。例如,想要查看一次指定的提交,假设你运
行gitlog命令并找到你增加了功能的那次提交:$gitlogcommit734713bc047d87bf7eac9
674765ae793478c50d3Author:ScottChaconDate
:FriJan218:32:332009-0800fixedrefshandling,addedgcaut
o,updatedtestscommitd921970aadf03b3cf0e71becdaab3147ba71cdef
Merge:1c002dd...35cfb2b...Author:ScottChaconcom>Date:ThuDec1115:08:432008-0800Mergecommit''phedders/
rdocs''commit1c002dd4b536e7479fe34593e72e6c6c1819e53bAuthor:Sc
ottChaconDate:ThuDec1114:58:322008-08
00addedsomeblameandmergestuff假设是1c002dd....。如果你想gitshow
这次提交,下面的命令是等价的(假设简短的版本没有歧义):$gitshow1c002dd4b536e7479fe34593e
72e6c6c1819e53b$gitshow1c002dd4b536e7479f$gitshow1c002dGi
t可以为你的SHA-1值生成出简短且唯一的缩写。如果你传递--abbrev-commit给gitlog命令,输出结
果里就会使用简短且唯一的值;它默认使用七个字符来表示,不过必要时为了避免SHA-1的歧义,会增加字符数:$gitlog-
-abbrev-commit--pretty=onelineca82a6dchangedtheversionnumbe
r085bb3bremovedunnecessarytestcodea11bef0firstcommit通常在一个
项目中,使用八到十个字符来避免SHA-1歧义已经足够了。最大的Git项目之一,Linux内核,目前也只需要最长40个
字符中的12个字符来保持唯一性。关于SHA-1的简短说明许多人可能会担心一个问题:在随机的偶然情况下,在他们的仓库里会出现
两个具有相同SHA-1值的对象。那会怎么样呢?如果你真的向仓库里提交了一个跟之前的某个对象具有相同SHA-1值的对象,Gi
t将会发现之前的那个对象已经存在在Git数据库中,并认为它已经被写入了。如果什么时候你想再次检出那个对象时,你会总是得到先前
的那个对象的数据。不过,你应该了解到,这种情况发生的概率是多么微小。SHA-1摘要长度是20字节,也就是160位。为了保
证有50%的概率出现一次冲突,需要2^80个随机哈希的对象(计算冲突机率的公式是p=(n(n-1)/2)(1/2
^160))。2^80是1.2x10^24,也就是一亿亿亿,那是地球上沙粒总数的1200倍。现在举例说一下怎样才能产生
一次SHA-1冲突。如果地球上65亿的人类都在编程,每人每秒都在产生等价于整个Linux内核历史(一百万个Git对
象)的代码,并将之提交到一个巨大的Git仓库里面,那将花费5年的时间才会产生足够的对象,使其拥有50%的概率产生一次
SHA-1对象冲突。这要比你编程团队的成员同一个晚上在互不相干的意外中被狼袭击并杀死的机率还要小。分支引用指明一次提交的最直接的
方法要求有一个指向它的分支引用。这样,你就可以在任何需要一个提交对象或者SHA-1值的Git命令中使用该分支名称了。如果你
想要显示一个分支的最后一次提交的对象,例如假设topic1分支指向ca82a6d,那么下面的命令是等价的:$gitshow
ca82a6dff817ec66f44342007202690a93763949$gitshowtopic1如果你想知道
某个分支指向哪个特定的SHA,或者想看任何一个例子中被简写的SHA-1,你可以使用一个叫做rev-parse的Git探
测工具。在第9章你可以看到关于探测工具的更多信息;简单来说,rev-parse是为了底层操作而不是日常操作设计的。不过,有时
你想看Git现在到底处于什么状态时,它可能会很有用。这里你可以对你的分支运执行rev-parse。$gitrev-pars
etopic1ca82a6dff817ec66f44342007202690a93763949引用日志里的简称在你工作的同时
,Git在后台的工作之一就是保存一份引用日志——一份记录最近几个月你的HEAD和分支引用的日志。你可以使用gitrefl
og来查看引用日志:$gitreflog734713b...HEAD@{0}:commit:fixedrefsh
andling,addedgcauto,updatedd921970...HEAD@{1}:mergephedde
rs/rdocs:Mergemadebyrecursive.1c002dd...HEAD@{2}:commit:a
ddedsomeblameandmergestuff1c36188...HEAD@{3}:rebase-i(s
quash):updatingHEAD95df984...HEAD@{4}:commit:#Thisisaco
mbinationoftwocommits.1c36188...HEAD@{5}:rebase-i(squash)
:updatingHEAD7e05da5...HEAD@{6}:rebase-i(pick):updatingH
EAD每次你的分支顶端因为某些原因被修改时,Git就会为你将信息保存在这个临时历史记录里面。你也可以使用这份数据来指明更早的分支
。如果你想查看仓库中HEAD在五次前的值,你可以使用引用日志的输出中的@{n}引用:$gitshowHEAD@{5}你
也可以使用这个语法来查看一定时间前分支指向哪里。例如,想看你的master分支昨天在哪,你可以输入$gitshowmas
ter@{yesterday}它就会显示昨天分支的顶端在哪。这项技术只对还在你引用日志里的数据有用,所以不能用来查看比几个月前还早
的提交。想要看类似于gitlog输出格式的引用日志信息,你可以运行gitlog-g:$gitlog-gmast
ercommit734713bc047d87bf7eac9674765ae793478c50d3Reflog:master
@{0}(ScottChacon)Reflogmessage:commit:f
ixedrefshandling,addedgcauto,updatedAuthor:ScottChacon<
schacon@gmail.com>Date:FriJan218:32:332009-0800fixedrefs
handling,addedgcauto,updatedtestscommitd921970aadf03b3cf0
e71becdaab3147ba71cdefReflog:master@{1}(ScottChacongmail.com>)Reflogmessage:mergephedders/rdocs:Mergemadebyr
ecursive.Author:ScottChaconDate:ThuDec
1115:08:432008-0800Mergecommit''phedders/rdocs''需要注意的是,日志引用信息
只存在于本地——这是一个你在仓库里做过什么的日志。其他人的仓库拷贝里的引用和你的相同;而你新克隆一个仓库的时候,引用日志是空的,因
为你在仓库里还没有操作。只有你克隆了一个项目至少两个月,gitshowHEAD@{2.months.ago}才会有用——如果
你是五分钟前克隆的仓库,将不会有结果返回。祖先引用另一种指明某次提交的常用方法是通过它的祖先。如果你在引用最后加上一个^,Git
将其理解为此次提交的父提交。假设你的工程历史是这样的:$gitlog--pretty=format:''%h%s''--
graph734713bfixedrefshandling,addedgcauto,updatedtests
d921970Mergecommit''phedders/rdocs''|\|35cfb2bSomerdoc
changes|1c002ddaddedsomeblameandmergestuff|/1c3618
8ignore.gem9b29157addopen3_detachtogemspecfilelist那么,
想看上一次提交,你可以使用HEAD^,意思是“HEAD的父提交”:$gitshowHEAD^commitd92197
0aadf03b3cf0e71becdaab3147ba71cdefMerge:1c002dd...35cfb2b...A
uthor:ScottChaconDate:ThuDec1115:08:43
2008-0800Mergecommit''phedders/rdocs''你也可以在^后添加一个数字——例如,d921
970^2意思是“d921970的第二父提交”。这种语法只在合并提交时有用,因为合并提交可能有多个父提交。第一父提交是你合并时
所在分支,而第二父提交是你所合并的分支:$gitshowd921970^commit1c002dd4b536e7479f
e34593e72e6c6c1819e53bAuthor:ScottChaconD
ate:ThuDec1114:58:322008-0800addedsomeblameandmergest
uff$gitshowd921970^2commit35cfb2b795a55793d7cc56a6cc2060b4b
b732548Author:PaulHedderlyDate:WedDec10
22:22:032008+0000Somerdocchanges另外一个指明祖先提交的方法是~。这也是指向第一父提交
,所以HEAD~和HEAD^是等价的。当你指定数字的时候就明显不一样了。HEAD~2是指“第一父提交的第一父提交”,也就
是“祖父提交”——它会根据你指定的次数检索第一父提交。例如,在上面列出的历史记录里面,HEAD~3会是$gitshowHE
AD~3commit1c3618887afb5fbcbea25b7c013f4e2114448b8dAuthor:Tom
Preston-WernerDate:FriNov713:47:592008-0
500ignore.gem也可以写成HEAD^^^,同样是第一父提交的第一父提交的第一父提交:$gitshowHEA
D^^^commit1c3618887afb5fbcbea25b7c013f4e2114448b8dAuthor:Tom
Preston-WernerDate:FriNov713:47:592008-0
500ignore.gem你也可以混合使用这些语法——你可以通过HEAD~3^2指明先前引用的第二父提交(假设它是一个合
并提交)。提交范围现在你已经可以指明单次的提交,让我们来看看怎样指明一定范围的提交。这在你管理分支的时候尤显重要——如果你有很多分
支,你可以指明范围来圈定一些问题的答案,比如:“这个分支上我有哪些工作还没合并到主分支的?”双点最常用的指明范围的方法是双点的语法
。这种语法主要是让Git区分出可从一个分支中获得而不能从另一个分支中获得的提交。例如,假设你有类似于图6-1的提交历史。图
6-1.范围选择的提交历史实例你想要查看你的试验分支上哪些没有被提交到主分支,那么你就可以使用master..experi
ment来让Git显示这些提交的日志——这句话的意思是“所有可从experiment分支中获得而不能从master分支中获得
的提交”。为了使例子简单明了,我使用了图标中提交对象的字母来代替真实日志的输出,所以会显示:$gitlogmaster..e
xperimentDC另一方面,如果你想看相反的——所有在master而不在experiment中的分支——你可以交换
分支的名字。experiment..master显示所有可在master获得而在experiment中不能的提交:$gi
tlogexperiment..masterFE这在你想保持experiment分支最新和预览你将合并的提交的时候特别
有用。这个语法的另一种常见用途是查看你将把什么推送到远程:$gitlogorigin/master..HEAD这条命令显示任
何在你当前分支上而不在远程origin上的提交。如果你运行gitpush并且的你的当前分支正在跟踪origin/maste
r,被gitlogorigin/master..HEAD列出的提交就是将被传输到服务器上的提交。你也可以留空语法中的一边来
让Git来假定它是HEAD。例如,输入gitlogorigin/master..将得到和上面的例子一样的结果——Gi
t使用HEAD来代替不存在的一边。多点双点语法就像速记一样有用;但是你也许会想针对两个以上的分支来指明修订版本,比如查看哪些
提交被包含在某些分支中的一个,但是不在你当前的分支上。Git允许你在引用前使用^字符或者--not指明你不希望提交被包含其中的分支
。因此下面三个命令是等同的:$gitlogrefA..refB$gitlog^refArefB$gitlog
refB--notrefA这样很好,因为它允许你在查询中指定多于两个的引用,而这是双点语法所做不到的。例如,如果你想查找所有
从refA或refB包含的但是不被refC包含的提交,你可以输入下面中的一个$gitlogrefArefB^refC$
gitlogrefArefB--notrefC这建立了一个非常强大的修订版本查询系统,应该可以帮助你解决分支里包含了什
么这个问题。三点最后一种主要的范围选择语法是三点语法,这个可以指定被两个引用中的一个包含但又不被两者同时包含的分支。回过头来看一下
图6-1里所列的提交历史的例子。如果你想查看master或者experiment中包含的但不是两者共有的引用,你可以运行$gi
tlogmaster...experimentFEDC这个再次给出你普通的log输出但是只显示那四次提交的信息,按照传
统的提交日期排列。这种情形下,log命令的一个常用参数是--left-right,它会显示每个提交到底处于哪一侧的分支。这使得数据
更加有用。$gitlog--left-rightmaster...experimentD>C有了
以上工具,让Git知道你要察看哪些提交就容易得多了。6.2?交互式暂存Git提供了很多脚本来辅助某些命令行任务。这里,你将看到一
些交互式命令,它们帮助你方便地构建只包含特定组合和部分文件的提交。在你修改了一大批文件然后决定将这些变更分布在几个各有侧重的提交而
不是单个又大又乱的提交时,这些工具非常有用。用这种方法,你可以确保你的提交在逻辑上划分为相应的变更集,以便于供和你一起工作的开发者
审阅。如果你运行gitadd时加上-i或者--interactive选项,Git就进入了一个交互式的shell模式,显示一些类似
于下面的信息:$gitadd-istagedunstagedpath1:unchanged+0/-1TODO
2:unchanged+1/-1index.html3:unchanged+5/-1lib/simplegit.rb
Commands1:status2:update3:revert4:adduntracked
5:patch6:diff7:quit8:helpWhatnow>你会看到这个命令以一个完全不同的视图显示了你
的暂存区——主要是你通过gitstatus得到的那些信息但是稍微简洁但信息更加丰富一些。它在左侧列出了你暂存的变更,在右侧列出了
未被暂存的变更。在这之后是一个命令区。这里你可以做很多事情,包括暂存文件,撤回文件,暂存部分文件,加入未被追踪的文件,查看暂存文件
的差别。暂存和撤回文件如果你在Whatnow>的提示后输入2或者u,这个脚本会提示你那些文件你想要暂存:Whatnow>2
stagedunstagedpath1:unchanged+0/-1TODO2:unchanged+1/-1i
ndex.html3:unchanged+5/-1lib/simplegit.rbUpdate>>如果想暂存TODO和i
ndex.html,你可以输入相应的编号:Update>>1,2stagedunstagedpath1:uncha
nged+0/-1TODO2:unchanged+1/-1index.html3:unchanged+5/-
1lib/simplegit.rbUpdate>>每个文件旁边的表示选中的文件将被暂存。如果你在update>>提示后直接敲
入回车,Git会替你把所有选中的内容暂存:Update>>updated2pathsCommands1:
status2:update3:revert4:adduntracked5:patch6:diff7:
quit8:helpWhatnow>1stagedunstagedpath1:+0/-1nothingTO
DO2:+1/-1nothingindex.html3:unchanged+5/-1lib/simplegit.r
b现在你可以看到TODO和index.html文件被暂存了同时simplegit.rb文件仍然未被暂存。如果这时你想要撤回TODO
文件,就使用3或者r(代表revert,恢复)选项:Commands1:status2:update3:
revert4:adduntracked5:patch6:diff7:quit8:helpWhatno
w>3stagedunstagedpath1:+0/-1nothingTODO2:+1/-1nothing
index.html3:unchanged+5/-1lib/simplegit.rbRevert>>1staged
unstagedpath1:+0/-1nothingTODO2:+1/-1nothingindex.html
3:unchanged+5/-1lib/simplegit.rbRevert>>[enter]revertedon
epath再次查看Git的状态,你会看到你已经撤回了TODO文件Commands1:status2:up
date3:revert4:adduntracked5:patch6:diff7:quit8:help
Whatnow>1stagedunstagedpath1:unchanged+0/-1TODO2:+1/-1
nothingindex.html3:unchanged+5/-1lib/simplegit.rb要查看你暂存内容的差
异,你可以使用6或者d(表示diff)命令。它会显示你暂存文件的列表,你可以选择其中的几个,显示其被暂存的差异。这跟你在命令行下指
定gitdiff--cached非常相似:Commands1:status2:update3:re
vert4:adduntracked5:patch6:diff7:quit8:helpWhatnow>
6stagedunstagedpath1:+1/-1nothingindex.htmlReviewdiff>>
1diff--gita/index.htmlb/index.htmlindex4d07108..4335f49100
644---a/index.html+++b/index.html@@-16,7+16,7@@DateFind
er...

-contact:support@github
.com+contact:email.support@github.comdiv>通过这些基本命令,你可以使用交互式增加模式更加方便地处理暂
存区。暂存补丁只让Git暂存文件的某些部分而忽略其他也是有可能的。例如,你对simplegit.rb文件作了两处修改但是只想暂存其
中一个而忽略另一个,在Git中实现这一点非常容易。在交互式的提示符下,输入5或者p(表示patch,补丁)。Git会询问哪些文件你
希望部分暂存;然后对于被选中文件的每一节,他会逐个显示文件的差异区块并询问你是否希望暂存他们:diff--gita/lib/s
implegit.rbb/lib/simplegit.rbindexdd5ecc4..57399e0100644---
a/lib/simplegit.rb+++b/lib/simplegit.rb@@-22,7+22,7@@class
SimpleGitenddeflog(treeish=''master'')-command("gitlog-n
25#{treeish}")+command("gitlog-n30#{treeish}")enddefbla
me(path)Stagethishunk[y,n,a,d,/,j,J,g,e,?]?此处你有很多选择。输入?可以显示列
表:Stagethishunk[y,n,a,d,/,j,J,g,e,?]??y-stagethishunkn
-donotstagethishunka-stagethisandalltheremaininghun
ksinthefiled-donotstagethishunknoranyoftheremainin
ghunksinthefileg-selectahunktogoto/-searchforah
unkmatchingthegivenregexj-leavethishunkundecided,seen
extundecidedhunkJ-leavethishunkundecided,seenexthunkk
-leavethishunkundecided,seepreviousundecidedhunkK-lea
vethishunkundecided,seeprevioushunks-splitthecurrenth
unkintosmallerhunkse-manuallyeditthecurrenthunk?-pri
nthelp如果你想暂存各个区块,通常你会输入y或者n,但是暂存特定文件里的全部区块或者暂时跳过对一个区块的处理同样也很有用。如
果你暂存了文件的一个部分而保留另外一个部分不被暂存,你的状态输出看起来会是这样:Whatnow>1stagedunstag
edpath1:unchanged+0/-1TODO2:+1/-1nothingindex.html3:+1
/-1+4/-0lib/simplegit.rbsimplegit.rb的状态非常有意思。它显示有几行被暂存了,有几行没有。你
部分地暂存了这个文件。在这时,你可以退出交互式脚本然后运行gitcommit来提交部分暂存的文件。最后你也可以不通过交互式增加的
模式来实现部分文件暂存——你可以在命令行下通过gitadd-p或者gitadd--patch来启动同样的脚本。6.3?储
藏(Stashing)经常有这样的事情发生,当你正在进行项目中某一部分的工作,里面的东西处于一个比较杂乱的状态,而你想转到其他分支
上进行一些工作。问题是,你不想提交进行了一半的工作,否则以后你无法回到这个工作点。解决这个问题的办法就是gitstash命令。“
‘储藏”“可以获取你工作目录的中间状态——也就是你修改过的被追踪的文件和暂存的变更——并将它保存到一个未完结变更的堆栈中,随时可以
重新应用。储藏你的工作为了演示这一功能,你可以进入你的项目,在一些文件上进行工作,有可能还暂存其中一个变更。如果你运行gits
tatus,你可以看到你的中间状态:$gitstatus#Onbranchmaster#Changestobe
committed:#(use"gitresetHEAD..."tounstage)##mod
ified:index.html##Changedbutnotupdated:#(use"gitaddile>..."toupdatewhatwillbecommitted)##modified:lib/simp
legit.rb#现在你想切换分支,但是你还不想提交你正在进行中的工作;所以你储藏这些变更。为了往堆栈推送一个新的储藏,只要运行
gitstash:$gitstashSavedworkingdirectoryandindexstate\
"WIPonmaster:049d078addedtheindexfile"HEADisnowat049d
078addedtheindexfile(Torestorethemtype"gitstashapply")
你的工作目录就干净了:$gitstatus#Onbranchmasternothingtocommit(wor
kingdirectoryclean)这时,你可以方便地切换到其他分支工作;你的变更都保存在栈上。要查看现有的储藏,你可以使用
gitstashlist:$gitstashliststash@{0}:WIPonmaster:049d07
8addedtheindexfilestash@{1}:WIPonmaster:c264051...Rever
t"addedfile_size"stash@{2}:WIPonmaster:21d80a5...addednu
mbertolog在这个案例中,之前已经进行了两次储藏,所以你可以访问到三个不同的储藏。你可以重新应用你刚刚实施的储藏,所采用
的命令就是之前在原始的stash命令的帮助输出里提示的:gitstashapply。如果你想应用更早的储藏,你可以通过名字
指定它,像这样:gitstashapplystash@{2}。如果你不指明,Git默认使用最近的储藏并尝试应用它:$gi
tstashapply#Onbranchmaster#Changedbutnotupdated:#(us
e"gitadd..."toupdatewhatwillbecommitted)##modif
ied:index.html#modified:lib/simplegit.rb#你可以看到Git重新修改了你所储藏
的那些当时尚未提交的文件。在这个案例里,你尝试应用储藏的工作目录是干净的,并且属于同一分支;但是一个干净的工作目录和应用到相同的分
支上并不是应用储藏的必要条件。你可以在其中一个分支上保留一份储藏,随后切换到另外一个分支,再重新应用这些变更。在工作目录里包含已修
改和未提交的文件时,你也可以应用储藏——Git会给出归并冲突如果有任何变更无法干净地被应用。对文件的变更被重新应用,但是被暂存的
文件没有重新被暂存。想那样的话,你必须在运行gitstashapply命令时带上一个--index的选项来告诉命令重新
应用被暂存的变更。如果你是这么做的,你应该已经回到你原来的位置:$gitstashapply--index#Onbr
anchmaster#Changestobecommitted:#(use"gitresetHEADle>..."tounstage)##modified:index.html##Changedbutnot
updated:#(use"gitadd..."toupdatewhatwillbecommit
ted)##modified:lib/simplegit.rb#apply选项只尝试应用储藏的工作——储藏的内容仍然在
栈上。要移除它,你可以运行gitstashdrop,加上你希望移除的储藏的名字:$gitstashliststash
@{0}:WIPonmaster:049d078addedtheindexfilestash@{1}:WIP
onmaster:c264051...Revert"addedfile_size"stash@{2}:WIPon
master:21d80a5...addednumbertolog$gitstashdropstash@{0}
Droppedstash@{0}(364e91f3f268f0900bc3ee613f9f733e82aaed43)你也可以
运行gitstashpop来重新应用储藏,同时立刻将其从堆栈中移走。Un-applyingaStashInsome
usecasescenariosyoumightwanttoapplystashedchanges,doso
mework,butthenun-applythosechangesthatoriginallycamefor
mthestash.Gitdoesnotprovidesuchastashunapplycommand,bu
titispossibletoachievetheeffectbysimplyretrievingthep
atchassociatedwithastashandapplyingitinreverse:$gitsta
shshow-pstash@{0}|gitapply-RAgain,ifyoudon’tspecifya
stash,Gitassumesthemostrecentstash:$gitstashshow-p|gi
tapply-RYoumaywanttocreateanaliasandeffectivelyaddas
tash-unapplycommandtoyourgit.Forexample:$gitconfig--glob
alalias.stash-unapply''!gitstashshow-p|gitapply-R''$git
stash$#...workworkwork$gitstash-unapply从储藏中创建分支如果你储藏了一些工
作,暂时不去理会,然后继续在你储藏工作的分支上工作,你在重新应用工作时可能会碰到一些问题。如果尝试应用的变更是针对一个你那之后修改
过的文件,你会碰到一个归并冲突并且必须去化解它。如果你想用更方便的方法来重新检验你储藏的变更,你可以运行gitstashbra
nch,这会创建一个新的分支,检出你储藏工作时的所处的提交,重新应用你的工作,如果成功,将会丢弃储藏。$gitstashbr
anchtestchangesSwitchedtoanewbranch"testchanges"#Onbran
chtestchanges#Changestobecommitted:#(use"gitresetHEAD
..."tounstage)##modified:index.html##Changedbutn
otupdated:#(use"gitadd..."toupdatewhatwillbecom
mitted)##modified:lib/simplegit.rb#Droppedrefs/stash@{0}(
f0dfc4d5dc332d1cee34a634182e168c4efc3359)这是一个很棒的捷径来恢复储藏的工作然后在新的分支
上继续当时的工作。?6.4?重写历史很多时候,在Git上工作的时候,你也许会由于某种原因想要修订你的提交历史。Git的一个
卓越之处就是它允许你在最后可能的时刻再作决定。你可以在你即将提交暂存区时决定什么文件归入哪一次提交,你可以使用stash命令来
决定你暂时搁置的工作,你可以重写已经发生的提交以使它们看起来是另外一种样子。这个包括改变提交的次序、改变说明或者修改提交中包含的文
件,将提交归并、拆分或者完全删除——这一切在你尚未开始将你的工作和别人共享前都是可以的。在这一节中,你会学到如何完成这些很有用的任
务以使你的提交历史在你将其共享给别人之前变成你想要的样子。改变最近一次提交改变最近一次提交也许是最常见的重写历史的行为。对于你的最
近一次提交,你经常想做两件基本事情:改变提交说明,或者改变你刚刚通过增加,改变,删除而记录的快照。如果你只想修改最近一次提交说明,
这非常简单:$gitcommit--amend这会把你带入文本编辑器,里面包含了你最近一次提交说明,供你修改。当你保存并退出
编辑器,这个编辑器会写入一个新的提交,里面包含了那个说明,并且让它成为你的新的最近一次提交。如果你完成提交后又想修改被提交的快照,
增加或者修改其中的文件,可能因为你最初提交时,忘了添加一个新建的文件,这个过程基本上一样。你通过修改文件然后对其运行gitadd
或对一个已被记录的文件运行gitrm,随后的gitcommit--amend会获取你当前的暂存区并将它作为新提交对应的快照。
使用这项技术的时候你必须小心,因为修正会改变提交的SHA-1值。这个很像是一次非常小的rebase——不要在你最近一次提交被推送后
还去修正它。修改多个提交说明要修改历史中更早的提交,你必须采用更复杂的工具。Git没有一个修改历史的工具,但是你可以使用rebas
e工具来衍合一系列的提交到它们原来所在的HEAD上而不是移到新的上。依靠这个交互式的rebase工具,你就可以停留在每一次提交后,
如果你想修改或改变说明、增加文件或任何其他事情。你可以通过给gitrebase增加-i选项来以交互方式地运行rebase。你必须
通过告诉命令衍合到哪次提交,来指明你需要重写的提交的回溯深度。例如,你想修改最近三次的提交说明,或者其中任意一次,你必须给git
rebase-i提供一个参数,指明你想要修改的提交的父提交,例如HEAD~2或者HEAD~3。可能记住~3更加容易,因为你想修改
最近三次提交;但是请记住你事实上所指的是四次提交之前,即你想修改的提交的父提交。$gitrebase-iHEAD~3再次提
醒这是一个衍合命令——HEAD~3..HEAD范围内的每一次提交都会被重写,无论你是否修改说明。不要涵盖你已经推送到中心服务器的提
交——这么做会使其他开发者产生混乱,因为你提供了同样变更的不同版本。运行这个命令会为你的文本编辑器提供一个提交列表,看起来像下面这
样pickf7f3f6dchangedmynameabitpick310154eupdatedREADMEf
ormattingandaddedblamepicka5f4a0daddedcat-file#Rebase71
0f0f8..a5f4a0donto710f0f8##Commands:#p,pick=usecommit
#e,edit=usecommit,butstopforamending#s,squash=usec
ommit,butmeldintopreviouscommit##Ifyouremovealineher
eTHATCOMMITWILLBELOST.#However,ifyouremoveeverything,
therebasewillbeaborted.#很重要的一点是你得注意这些提交的顺序与你通常通过log命令看到的是相反的
。如果你运行log,你会看到下面这样的结果:$gitlog--pretty=format:"%h%s"HEAD~3..H
EADa5f4a0daddedcat-file310154eupdatedREADMEformattingand
addedblamef7f3f6dchangedmynameabit请注意这里的倒序。交互式的rebase给了你一个
即将运行的脚本。它会从你在命令行上指明的提交开始(HEAD~3)然后自上至下重播每次提交里引入的变更。它将最早的列在顶上而不是最近
的,因为这是第一个需要重播的。你需要修改这个脚本来让它停留在你想修改的变更上。要做到这一点,你只要将你想修改的每一次提交前面的pi
ck改为edit。例如,只想修改第三次提交说明的话,你就像下面这样修改文件:editf7f3f6dchangedmynam
eabitpick310154eupdatedREADMEformattingandaddedblamepi
cka5f4a0daddedcat-file当你保存并退出编辑器,Git会倒回至列表中的最后一次提交,然后把你送到命令行中,
同时显示以下信息:$gitrebase-iHEAD~3Stoppedat7482e0d...updatedthe
gemspectohopefullyworkbetterYoucanamendthecommitnow,w
ithgitcommit--amendOnceyou’resatisfiedwithyourchanges,r
ungitrebase--continue这些指示很明确地告诉了你该干什么。输入$gitcommit--amend修改
提交说明,退出编辑器。然后,运行$gitrebase--continue这个命令会自动应用其他两次提交,你就完成任务了。如果
你将更多行的pick改为edit,你就能对你想修改的提交重复这些步骤。Git每次都会停下,让你修正提交,完成后继续运行。重
排提交你也可以使用交互式的衍合来彻底重排或删除提交。如果你想删除”addedcat-file”这个提交并且修改其他两次提交引入的
顺序,你将rebase脚本从这个pickf7f3f6dchangedmynameabitpick310154eu
pdatedREADMEformattingandaddedblamepicka5f4a0daddedcat-f
ile改为这个:pick310154eupdatedREADMEformattingandaddedblamepi
ckf7f3f6dchangedmynameabit当你保存并退出编辑器,Git将分支倒回至这些提交的父提交,应用3
10154e,然后f7f3f6d,接着停止。你有效地修改了这些提交的顺序并且彻底删除了”addedcat-file”这次提交。压
制(Squashing)提交交互式的衍合工具还可以将一系列提交压制为单一提交。脚本在rebase的信息里放了一些有用的指示:#
#Commands:#p,pick=usecommit#e,edit=usecommit,buts
topforamending#s,squash=usecommit,butmeldintoprevious
commit##IfyouremovealinehereTHATCOMMITWILLBELOST.#
However,ifyouremoveeverything,therebasewillbeaborted.#
如果不用”pick”或者”edit”,而是指定”squash”,Git会同时应用那个变更和它之前的变更并将提交说明归并。因此,如
果你想将这三个提交合并为单一提交,你可以将脚本修改成这样:pickf7f3f6dchangedmynameabits
quash310154eupdatedREADMEformattingandaddedblamesquasha5
f4a0daddedcat-file当你保存并退出编辑器,Git会应用全部三次变更然后将你送回编辑器来归并三次提交说明。#
Thisisacombinationof3commits.#Thefirstcommit''smessage
is:changedmynameabit#Thisisthe2ndcommitmessage:updat
edREADMEformattingandaddedblame#Thisisthe3rdcommitmes
sage:addedcat-file当你保存之后,你就拥有了一个包含前三次提交的全部变更的单一提交。拆分提交拆分提交就是撤销一
次提交,然后多次部分地暂存或提交直到结束。例如,假设你想将三次提交中的中间一次拆分。将”updatedREADMEformat
tingandaddedblame”拆分成两次提交:第一次为”updatedREADMEformatting”,第二次为
”addedblame”。你可以在rebase-i脚本中修改你想拆分的提交前的指令为”edit”:pickf7f3f6dc
hangedmynameabitedit310154eupdatedREADMEformattinganda
ddedblamepicka5f4a0daddedcat-file然后,这个脚本就将你带入命令行,你重置那次提交,提取被
重置的变更,从中创建多次提交。当你保存并退出编辑器,Git倒回到列表中第一次提交的父提交,应用第一次提交(f7f3f6d),应用
第二次提交(310154e),然后将你带到控制台。那里你可以用gitresetHEAD^对那次提交进行一次混合的重置,这将撤销
那次提交并且将修改的文件撤回。此时你可以暂存并提交文件,直到你拥有多次提交,结束后,运行gitrebase--continue
。$gitresetHEAD^$gitaddREADME$gitcommit-m''updatedREAD
MEformatting''$gitaddlib/simplegit.rb$gitcommit-m''added
blame''$gitrebase--continueGit在脚本中应用了最后一次提交(a5f4a0d),你的历史看起来就像
这样了:$gitlog-4--pretty=format:"%h%s"1c002ddaddedcat-file9
b29157addedblame35cfb2bupdatedREADMEformattingf3cc40echan
gedmynameabit再次提醒,这会修改你列表中的提交的SHA值,所以请确保这个列表里不包含你已经推送到共享仓库的
提交。核弹级选项:filter-branch如果你想用脚本的方式修改大量的提交,还有一个重写历史的选项可以用——例如,全局性地修
改电子邮件地址或者将一个文件从所有提交中删除。这个命令是filter-branch,这个会大面积地修改你的历史,所以你很有可能不该
去用它,除非你的项目尚未公开,没有其他人在你准备修改的提交的基础上工作。尽管如此,这个可以非常有用。你会学习一些常见用法,借此对它
的能力有所认识。从所有提交中删除一个文件这个经常发生。有些人不经思考使用gitadd.,意外地提交了一个巨大的二进制文件,你想
将它从所有地方删除。也许你不小心提交了一个包含密码的文件,而你想让你的项目开源。filter-branch大概会是你用来清理整个历
史的工具。要从整个历史中删除一个名叫password.txt的文件,你可以在filter-branch上使用--tree-filt
er选项:$gitfilter-branch--tree-filter''rm-fpasswords.txt''HEAD
Rewrite6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd(21/21)Ref''re
fs/heads/master''wasrewritten--tree-filter选项会在每次检出项目时先执行指定的命令然后重
新提交结果。在这个例子中,你会在所有快照中删除一个名叫password.txt的文件,无论它是否存在。如果你想删除所有不小心提
交上去的编辑器备份文件,你可以运行类似gitfilter-branch--tree-filter''rm-f~''HEA
D的命令。你可以观察到Git重写目录树并且提交,然后将分支指针移到末尾。一个比较好的办法是在一个测试分支上做这些然后在你确定产
物真的是你所要的之后,再hard-reset你的主分支。要在你所有的分支上运行filter-branch的话,你可以传递一个-
-all给命令。将一个子目录设置为新的根目录假设你完成了从另外一个代码控制系统的导入工作,得到了一些没有意义的子目录(trunk,
tags等等)。如果你想让trunk子目录成为每一次提交的新的项目根目录,filter-branch也可以帮你做到:$git
filter-branch--subdirectory-filtertrunkHEADRewrite856f0bf61e
41a27326cdae8f09fe708d679f596f(12/12)Ref''refs/heads/master''wa
srewritten现在你的项目根目录就是trunk子目录了。Git会自动地删除不对这个子目录产生影响的提交。全局性地更换电子
邮件地址另一个常见的案例是你在开始时忘了运行gitconfig来设置你的姓名和电子邮件地址,也许你想开源一个项目,把你所有的工作
电子邮件地址修改为个人地址。无论哪种情况你都可以用filter-branch来更换多次提交里的电子邮件地址。你必须小心一些,只改变
属于你的电子邮件地址,所以你使用--commit-filter:$gitfilter-branch--commit-filt
er''if["$GIT_AUTHOR_EMAIL"="schacon@localhost"];thenGIT_A
UTHOR_NAME="ScottChacon";GIT_AUTHOR_EMAIL="schacon@example.com"
;gitcommit-tree"$@";elsegitcommit-tree"$@";fi''HEAD这个会遍历并
重写所有提交使之拥有你的新地址。因为提交里包含了它们的父提交的SHA-1值,这个命令会修改你的历史中的所有提交,而不仅仅是包含了匹
配的电子邮件地址的那些。?6.5?使用Git调试Git同样提供了一些工具来帮助你调试项目中遇到的问题。由于Git被设计
为可应用于几乎任何类型的项目,这些工具是通用型,但是在遇到问题时可以经常帮助你查找缺陷所在。文件标注如果你在追踪代码中的缺陷想知道
这是什么时候为什么被引进来的,文件标注会是你的最佳工具。它会显示文件中对每一行进行修改的最近一次提交。因此,如果你发现自己代码中的
一个方法存在缺陷,你可以用gitblame来标注文件,查看那个方法的每一行分别是由谁在哪一天修改的。下面这个例子使用了-L选项来
限制输出范围在第12至22行:$gitblame-L12,22simplegit.rb^4832fe2(Scott
Chacon2008-03-1510:31:28-070012)defshow(tree=''master'')^4
832fe2(ScottChacon2008-03-1510:31:28-070013)command("gits
how#{tree}")^4832fe2(ScottChacon2008-03-1510:31:28-070014
)end^4832fe2(ScottChacon2008-03-1510:31:28-070015)9f6560
e4(ScottChacon2008-03-1721:52:20-070016)deflog(tree=''ma
ster'')79eaf55d(ScottChacon2008-04-0610:15:08-070017)comma
nd("gitlog#{tree}")9f6560e4(ScottChacon2008-03-1721:52:20
-070018)end9f6560e4(ScottChacon2008-03-1721:52:20-070019
)42cf2861(MagnusChacon2008-04-1310:45:01-070020)defblame
(path)42cf2861(MagnusChacon2008-04-1310:45:01-070021)comm
and("gitblame#{path}")42cf2861(MagnusChacon2008-04-1310:45
:01-070022)end请注意第一个域里是最后一次修改该行的那次提交的SHA-1值。接下去的两个域是从那次提交中抽取
的值——作者姓名和日期——所以你可以方便地获知谁在什么时候修改了这一行。在这后面是行号和文件的内容。请注意^4832fe2提交的那
些行,这些指的是文件最初提交的那些行。那个提交是文件第一次被加入这个项目时存在的,自那以后未被修改过。这会带来小小的困惑,因为你已
经至少看到了Git使用^来修饰一个提交的SHA值的三种不同的意义,但这里确实就是这个意思。另一件很酷的事情是在Git中你不需要
显式地记录文件的重命名。它会记录快照然后根据现实尝试找出隐式的重命名动作。这其中有一个很有意思的特性就是你可以让它找出所有的代码移
动。如果你在gitblame后加上-C,Git会分析你在标注的文件然后尝试找出其中代码片段的原始出处,如果它是从其他地方拷贝过来
的话。最近,我在将一个名叫GITServerHandler.m的文件分解到多个文件中,其中一个是GITPackUpload.m。通
过对GITPackUpload.m执行带-C参数的blame命令,我可以看到代码块的原始出处:$gitblame-C-L
141,153GITPackUpload.mf344f58dGITServerHandler.m(Scott2009-0
1-04141)f344f58dGITServerHandler.m(Scott2009-01-04142)-(v
oid)gatherObjectShasFromCf344f58dGITServerHandler.m(Scott200
9-01-04143){70befdddGITServerHandler.m(Scott2009-03-22144)
//NSLog(@"GATHERCOMMIad11ac80GITPackUpload.m(Scott2009-03-2
4145)ad11ac80GITPackUpload.m(Scott2009-03-24146)NSString
parentSha;ad11ac80GITPackUpload.m(Scott2009-03-24147)GITCom
mitcommit=[gad11ac80GITPackUpload.m(Scott2009-03-24148)
ad11ac80GITPackUpload.m(Scott2009-03-24149)//NSLog(@"GATHER
COMMIad11ac80GITPackUpload.m(Scott2009-03-24150)56ef2cafGI
TServerHandler.m(Scott2009-01-05151)if(commit){56ef2cafGIT
ServerHandler.m(Scott2009-01-05152)[refDictsetOb56ef2cafGI
TServerHandler.m(Scott2009-01-05153)这真的非常有用。通常,你会把你拷贝代码的那次提交作为
原始提交,因为这是你在这个文件中第一次接触到那几行。Git可以告诉你编写那些行的原始提交,即便是在另一个文件里。二分查找标注文件在
你知道问题是哪里引入的时候会有帮助。如果你不知道,并且自上次代码可用的状态已经经历了上百次的提交,你可能就要求助于bisect命令
了。bisect会在你的提交历史中进行二分查找来尽快地确定哪一次提交引入了错误。例如你刚刚推送了一个代码发布版本到产品环境中,对代
码为什么会表现成那样百思不得其解。你回到你的代码中,还好你可以重现那个问题,但是找不到在哪里。你可以对代码执行bisect来寻找。
首先你运行gitbisectstart启动,然后你用gitbisectbad来告诉系统当前的提交已经有问题了。然后你必须告
诉bisect已知的最后一次正常状态是哪次提交,使用gitbisectgood[good_commit]:$gitbis
ectstart$gitbisectbad$gitbisectgoodv1.0Bisecting:6re
visionslefttotestafterthis[ecb6e1bc347ccecc5f9350d878ce677f
eb13d3b2]errorhandlingonrepoGit发现在你标记为正常的提交(v1.0)和当前的错误版本之间有
大约12次提交,于是它检出中间的一个。在这里,你可以运行测试来检查问题是否存在于这次提交。如果是,那么它是在这个中间提交之前的某一
次引入的;如果否,那么问题是在中间提交之后引入的。假设这里是没有错误的,那么你就通过gitbisectgood来告诉Git
然后继续你的旅程:$gitbisectgoodBisecting:3revisionslefttotestaf
terthis[b047b02ea83310a70fd603dc8cd7a6cd13d15c04]securethist
hing现在你在另外一个提交上了,在你刚刚测试通过的和一个错误提交的中点处。你再次运行测试然后发现这次提交是错误的,因此你通过gi
tbisectbad来告诉Git:$gitbisectbadBisecting:1revisionsleftt
otestafterthis[f71ce38690acf49c1f3c9bea38e09d82a5ce6014]drop
exceptionstable这次提交是好的,那么Git就获得了确定问题引入位置所需的所有信息。它告诉你第一个错误提交的
SHA-1值并且显示一些提交说明以及哪些文件在那次提交里修改过,这样你可以找出缺陷被引入的根源:$gitbisectgoo
db047b02ea83310a70fd603dc8cd7a6cd13d15c04isfirstbadcommitco
mmitb047b02ea83310a70fd603dc8cd7a6cd13d15c04Author:PJHyettjhyett@example.com>Date:TueJan2714:48:322009-0800securet
histhing:04000004000040ee3e7821b895e52c1695092db9bdc4c61d1730
f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4Mconfig当你完成之后,你应该运行git
bisectreset来重设你的HEAD到你开始前的地方,否则你会处于一个诡异的地方:$gitbisectreset这是
个强大的工具,可以帮助你检查上百的提交,在几分钟内找出缺陷引入的位置。事实上,如果你有一个脚本会在工程正常时返回0,错误时返回非0
的话,你可以完全自动地执行gitbisect。首先你需要提供已知的错误和正确提交来告诉它二分查找的范围。你可以通过bisect
start命令来列出它们,先列出已知的错误提交再列出已知的正确提交:$gitbisectstartHEADv1.0$
gitbisectruntest-error.sh这样会自动地在每一个检出的提交里运行test-error.sh直到Git找
出第一个破损的提交。你也可以运行像make或者maketests或者任何你所拥有的来为你执行自动化的测试。?6.6?子模块经常
有这样的事情,当你在一个项目上工作时,你需要在其中使用另外一个项目。也许它是一个第三方开发的库或者是你独立开发和并在多个父项目中使
用的。这个场景下一个常见的问题产生了:你想将两个项目单独处理但是又需要在其中一个中使用另外一个。这里有一个例子。假设你在开发一个网
站,为之创建Atom源。你不想编写一个自己的Atom生成代码,而是决定使用一个库。你可能不得不像CPANinstall或者Rub
ygem一样包含来自共享库的代码,或者将代码拷贝到你的项目树中。如果采用包含库的办法,那么不管用什么办法都很难去定制这个库,部署
它就更加困难了,因为你必须确保每个客户都拥有那个库。把代码包含到你自己的项目中带来的问题是,当上游被修改时,任何你进行的定制化的修
改都很难归并。Git通过子模块处理这个问题。子模块允许你将一个Git仓库当作另外一个Git仓库的子目录。这允许你克隆另外一个
仓库到你的项目中并且保持你的提交相对独立。子模块初步假设你想把Rack库(一个Ruby的web服务器网关接口)加入到你
的项目中,可能既要保持你自己的变更,又要延续上游的变更。首先你要把外部的仓库克隆到你的子目录中。你通过gitsubmodule
add将外部项目加为子模块:$gitsubmoduleaddgit://github.com/chneukirchen/r
ack.gitrackInitializedemptyGitrepositoryin/opt/subtest/rac
k/.git/remote:Countingobjects:3181,done.remote:Compressing
objects:100%(1534/1534),done.remote:Total3181(delta1951)
,reused2623(delta1603)Receivingobjects:100%(3181/3181),6
75.42KiB|422KiB/s,done.Resolvingdeltas:100%(1951/1951),
done.现在你就在项目里的rack子目录下有了一个Rack项目。你可以进入那个子目录,进行变更,加入你自己的远程可写仓库来推
送你的变更,从原始仓库拉取和归并等等。如果你在加入子模块后立刻运行gitstatus,你会看到下面两项:$gitstatus
#Onbranchmaster#Changestobecommitted:#(use"gitreset
HEAD..."tounstage)##newfile:.gitmodules#newfile:
rack#首先你注意到有一个.gitmodules文件。这是一个配置文件,保存了项目URL和你拉取到的本地子目录$cat
.gitmodules[submodule"rack"]path=rackurl=git://github.co
m/chneukirchen/rack.git如果你有多个子模块,这个文件里会有多个条目。很重要的一点是这个文件跟其他文件一样也是
处于版本控制之下的,就像你的.gitignore文件一样。它跟项目里的其他文件一样可以被推送和拉取。这是其他克隆此项目的人获知子模
块项目来源的途径。gitstatus的输出里所列的另一项目是rack。如果你运行在那上面运行gitdiff,会发现一些有趣
的东西:$gitdiff--cachedrackdiff--gita/rackb/racknewfilemo
de160000index0000000..08d709f---/dev/null+++b/rack@@-0,0
+1@@+Subprojectcommit08d709f78b8c5b0fbeb7821e37fa53e69afcf43
3尽管rack是你工作目录里的子目录,但Git把它视作一个子模块,当你不在那个目录里时并不记录它的内容。取而代之的是,Git
将它记录成来自那个仓库的一个特殊的提交。当你在那个子目录里修改并提交时,子项目会通知那里的HEAD已经发生变更并记录你当前正在
工作的那个提交;通过那样的方法,当其他人克隆此项目,他们可以重新创建一致的环境。这是关于子模块的重要一点:你记录他们当前确切所处的
提交。你不能记录一个子模块的master或者其他的符号引用。当你提交时,会看到类似下面的:$gitcommit-m''fir
stcommitwithsubmodulerack''[master0550271]firstcommitwith
submodulerack2fileschanged,4insertions(+),0deletions(-)
createmode100644.gitmodulescreatemode160000rack注意rack条目的
160000模式。这在Git中是一个特殊模式,基本意思是你将一个提交记录为一个目录项而不是子目录或者文件。你可以将rack目录
当作一个独立的项目,保持一个指向子目录的最新提交的指针然后反复地更新上层项目。所有的Git命令都在两个子目录里独立工作:$git
log-1commit0550271328a0038865aad6331e620cd7238601bbAuthor:S
cottChaconDate:ThuApr909:03:562009-07
00firstcommitwithsubmodulerack$cdrack/$gitlog-1commi
t08d709f78b8c5b0fbeb7821e37fa53e69afcf433Author:ChristianNeuk
irchenDate:WedMar2514:49:042009+0
100Documentversionchange克隆一个带子模块的项目这里你将克隆一个带子模块的项目。当你接收到这样一个项
目,你将得到了包含子项目的目录,但里面没有文件:$gitclonegit://github.com/schacon/mypr
oject.gitInitializedemptyGitrepositoryin/opt/myproject/.git
/remote:Countingobjects:6,done.remote:Compressingobjects:
100%(4/4),done.remote:Total6(delta0),reused0(delta0)
Receivingobjects:100%(6/6),done.$cdmyproject$ls-ltotal
8-rw-r--r--1schaconadmin3Apr909:11READMEdrwxr-xr-x2s
chaconadmin68Apr909:11rack$lsrack/$rack目录存在了,但是是空的。你必须运
行两个命令:gitsubmoduleinit来初始化你的本地配置文件,gitsubmoduleupdate来从那个项目拉取
所有数据并检出你上层项目里所列的合适的提交:$gitsubmoduleinitSubmodule''rack''(git:
//github.com/chneukirchen/rack.git)registeredforpath''rack''$
gitsubmoduleupdateInitializedemptyGitrepositoryin/opt/myp
roject/rack/.git/remote:Countingobjects:3181,done.remote:C
ompressingobjects:100%(1534/1534),done.remote:Total3181(d
elta1951),reused2623(delta1603)Receivingobjects:100%(318
1/3181),675.42KiB|173KiB/s,done.Resolvingdeltas:100%(19
51/1951),done.Submodulepath''rack'':checkedout''08d709f78b8c5
b0fbeb7821e37fa53e69afcf433''现在你的rack子目录就处于你先前提交的确切状态了。如果另外一个开发者变更
了rack的代码并提交,你拉取那个引用然后归并之,将得到稍有点怪异的东西:$gitmergeorigin/master
Updating0550271..85a3eeeFastforwardrack|2+-1fileschange
d,1insertions(+),1deletions(-)[master]$gitstatus#Onbra
nchmaster#Changedbutnotupdated:#(use"gitadd..."
toupdatewhatwillbecommitted)#(use"gitcheckout--.
.."todiscardchangesinworkingdirectory)##modified:rack#
你归并来的仅仅上是一个指向你的子模块的指针;但是它并不更新你子模块目录里的代码,所以看起来你的工作目录处于一个临时状态:$git
diffdiff--gita/rackb/rackindex6c5e70b..08d709f160000---
a/rack+++b/rack@@-1+1@@-Subprojectcommit6c5e70b984a60b3c
ecd395edd5b48a7575bf58e0+Subprojectcommit08d709f78b8c5b0fbeb78
21e37fa53e69afcf433事情就是这样,因为你所拥有的子模块的指针并对应于子模块目录的真实状态。为了修复这一点,你必须
再次运行gitsubmoduleupdate:$gitsubmoduleupdateremote:Counting
objects:5,done.remote:Compressingobjects:100%(3/3),done.
remote:Total3(delta1),reused2(delta0)Unpackingobjects:
100%(3/3),done.Fromgit@github.com:schacon/rack08d709f..6c5e7
0bmaster->origin/masterSubmodulepath''rack'':checkedout''6c
5e70b984a60b3cecd395edd5b48a7575bf58e0''每次你从主项目中拉取一个子模块的变更都必须这样做。看
起来很怪但是管用。一个常见问题是当开发者对子模块做了一个本地的变更但是并没有推送到公共服务器。然后他们提交了一个指向那个非公开状态
的指针然后推送上层项目。当其他开发者试图运行gitsubmoduleupdate,那个子模块系统会找不到所引用的提交,因为它只
存在于第一个开发者的系统中。如果发生那种情况,你会看到类似这样的错误:$gitsubmoduleupdatefatal:
referenceisn’tatree:6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Unabletocheckout''6c5e70b984a60b3cecd395edd5ba7575bf58e0''insu
bmodulepath''rack''你不得不去查看谁最后变更了子模块$gitlog-1rackcommit85a3e
ee996800fcfa91e2119372dd4172bf76678Author:ScottChacon@gmail.com>Date:ThuApr909:19:142009-0700addedasubmodule
referenceIwillnevermakepublic.hahahahaha!然后,你给那个家伙发电子邮件说他一
通。上层项目有时候,开发者想按照他们的分组获取一个大项目的子目录的子集。如果你是从CVS或者Subversion迁移过来的
话这个很常见,在那些系统中你已经定义了一个模块或者子目录的集合,而你想延续这种类型的工作流程。在Git中实现这个的一个好办法是
你将每一个子目录都做成独立的Git仓库,然后创建一个上层项目的Git仓库包含多个子模块。这个办法的一个优势是你可以在上层项
目中通过标签和分支更为明确地定义项目之间的关系。子模块的问题使用子模块并非没有任何缺点。首先,你在子模块目录中工作时必须相对小心。
当你运行gitsubmoduleupdate,它会检出项目的指定版本,但是不在分支内。这叫做获得一个分离的头——这意味着HE
AD文件直接指向一次提交,而不是一个符号引用。问题在于你通常并不想在一个分离的头的环境下工作,因为太容易丢失变更了。如果你先执行
了一次submoduleupdate,然后在那个子模块目录里不创建分支就进行提交,然后再次从上层项目里运行gitsubmodu
leupdate同时不进行提交,Git会毫无提示地覆盖你的变更。技术上讲你不会丢失工作,但是你将失去指向它的分支,因此会很难取到
。为了避免这个问题,当你在子模块目录里工作时应使用gitcheckout-bwork创建一个分支。当你再次在子模块里更新的时
候,它仍然会覆盖你的工作,但是至少你拥有一个可以回溯的指针。切换带有子模块的分支同样也很有技巧。如果你创建一个新的分支,增加了一个
子模块,然后切换回不带该子模块的分支,你仍然会拥有一个未被追踪的子模块的目录$gitcheckout-brackSwit
chedtoanewbranch"rack"$gitsubmoduleaddgit@github.com:sc
hacon/rack.gitrackInitializedemptyGitrepositoryin/opt/mypr
oj/rack/.git/...Receivingobjects:100%(3184/3184),677.42KiB
|34KiB/s,done.Resolvingdeltas:100%(1952/1952),done.$gi
tcommit-am''addedracksubmodule''[rackcc49a69]addedracksub
module2fileschanged,4insertions(+),0deletions(-)createmo
de100644.gitmodulescreatemode160000rack$gitcheckoutmast
erSwitchedtobranch"master"$gitstatus#Onbranchmaster#
Untrackedfiles:#(use"gitadd..."toincludeinwhatwi
llbecommitted)##rack/你将不得不将它移走或者删除,这样的话当你切换回去的时候必须重新克隆它——你可能
会丢失你未推送的本地的变更或分支。最后一个需要引起注意的是关于从子目录切换到子模块的。如果你已经跟踪了你项目中的一些文件但是想把它
们移到子模块去,你必须非常小心,否则Git会生你的气。假设你的项目中有一个子目录里放了rack的文件,然后你想将它转换为子模块
。如果你删除子目录然后运行submoduleadd,Git会向你大吼:$rm-Rfrack/$gitsubmodul
eaddgit@github.com:schacon/rack.gitrack''rack''alreadyexists
intheindex你必须先将rack目录撤回。然后你才能加入子模块:$gitrm-rrack$gitsubmo
duleaddgit@github.com:schacon/rack.gitrackInitializedemptyG
itrepositoryin/opt/testsub/rack/.git/remote:Countingobjects
:3184,done.remote:Compressingobjects:100%(1465/1465),done
.remote:Total3184(delta1952),reused2770(delta1675)Recei
vingobjects:100%(3184/3184),677.42KiB|88KiB/s,done.Reso
lvingdeltas:100%(1952/1952),done.现在假设你在一个分支里那样做了。如果你尝试切换回一个仍然
在目录里保留那些文件而不是子模块的分支时——你会得到下面的错误:$gitcheckoutmastererror:Untr
ackedworkingtreefile''rack/AUTHORS''wouldbeoverwrittenbyme
rge.你必须先移除rack子模块的目录才能切换到不包含它的分支:$mvrack/tmp/$gitcheckoutm
asterSwitchedtobranch"master"$lsREADMErack然后,当你切换回来,你会得到一
个空的rack目录。你可以运行gitsubmoduleupdate重新克隆,也可以将/tmp/rack目录重新移回空目录。6.7?子树合并现在你已经看到了子模块系统的麻烦之处,让我们来看一下解决相同问题的另一途径。当Git归并时,它会检查需要归并的内容然后选择一个合适的归并策略。如果你归并的分支是两个,Git使用一个_递归_策略。如果你归并的分支超过两个,Git采用_章鱼_策略。这些策略是自动选择的,因为递归策略可以处理复杂的三路归并情况——比如多于一个共同祖先的——但是它只能处理两个分支的归并。章鱼归并可以处理多个分支但是但必须更加小心以避免冲突带来的麻烦,因此它被选中作为归并两个以上分支的默认策略。实际上,你也可以选择其他策略。其中的一个就是_子树_归并,你可以用它来处理子项目问题。这里你会看到如何换用子树归并的方法来实现前一节里所做的rack的嵌入。子树归并的思想是你拥有两个工程,其中一个项目映射到另外一个项目的子目录中,反过来也一样。当你指定一个子树归并,Git可以聪明地探知其中一个是另外一个的子树从而实现正确的归并——这相当神奇。首先你将Rack应用加入到项目中。你将Rack项目当作你项目中的一个远程引用,然后将它检出到它自身的分支:$gitremoteaddrack_remotegit@github.com:schacon/rack.git$gitfetchrack_remotewarning:nocommoncommitsremote:Countingobjects:3184,done.remote:Compressingobjects:100%(1465/1465),done.remote:Total3184(delta1952),reused2770(delta1675)Receivingobjects:100%(3184/3184),677.42KiB|4KiB/s,done.Resolvingdeltas:100%(1952/1952),done.Fromgit@github.com:schacon/rack[newbranch]build->rack_remote/build[newbranch]master->rack_remote/master[newbranch]rack-0.4->rack_remote/rack-0.4[newbranch]rack-0.9->rack_remote/rack-0.9$gitcheckout-brack_branchrack_remote/masterBranchrack_branchsetuptotrackremotebranchrefs/remotes/rack_remote/master.Switchedtoanewbranch"rack_branch"现在在你的rack_branch分支中就有了Rack项目的根目录,而你自己的项目在master分支中。如果你先检出其中一个然后另外一个,你会看到它们有不同的项目根目录:$lsAUTHORSKNOWN-ISSUESRakefilecontriblibCOPYINGREADMEbinexampletest$gitcheckoutmasterSwitchedtobranch"master"$lsREADME要将Rack项目当作子目录拉取到你的master项目中。你可以在Git中用gitread-tree来实现。你会在第9章学到更多与read-tree和它的朋友相关的东西,当前你会知道它读取一个分支的根目录树到当前的暂存区和工作目录。你只要切换回你的master分支,然后拉取rack分支到你主项目的master分支的rack子目录:$gitread-tree--prefix=rack/-urack_branch当你提交的时候,看起来就像你在那个子目录下拥有Rack的文件——就像你从一个tarball里拷贝的一样。有意思的是你可以比较容易地归并其中一个分支的变更到另外一个。因此,如果Rack项目更新了,你可以通过切换到那个分支并执行拉取来获得上游的变更:$gitcheckoutrack_branch$gitpull然后,你可以将那些变更归并回你的master分支。你可以使用gitmerge-ssubtree,它会工作的很好;但是Git同时会把历史归并到一起,这可能不是你想要的。为了拉取变更并预置提交说明,需要在-ssubtree策略选项的同时使用--squash和--no-commit选项。$gitcheckoutmaster$gitmerge--squash-ssubtree--no-commitrack_branchSquashcommit--notupdatingHEADAutomaticmergewentwell;stoppedbeforecommittingasrequested所有Rack项目的变更都被归并可以进行本地提交。你也可以做相反的事情——在你主分支的rack目录里进行变更然后归并回rack_branch分支,然后将它们提交给维护者或者推送到上游。为了得到rack子目录和你rack_branch分支的区别——以决定你是否需要归并它们——你不能使用一般的diff命令。而是对你想比较的分支运行gitdiff-tree:$gitdiff-tree-prack_branch或者,为了比较你的rack子目录和服务器上你拉取时的master分支,你可以运行$gitdiff-tree-prack_remote/master6.8?总结你已经看到了很多高级的工具,允许你更加精确地操控你的提交和暂存区。当你碰到问题时,你应该可以很容易找出是哪个分支什么时候由谁引入了它们。如果你想在项目中使用子项目,你也已经学会了一些方法来满足这些需求。到此,你应该能够完成日常里你需要用命令行在Git下做的大部分事情,并且感到比较顺手。
献花(0)
+1
(本文系关平藏书首藏)