原文链接:25 Tips for Intermediate Git Users
作者:Andy Jeffries
时间:2009年11月1日
更新:这篇文章最初是在 2009年11月 发布到我的博客,它一直没有更新——不过有许多人发现这篇文章很有用,所以我想保持下去。请不要评论说“这些已经不再是中级小贴士了”。
我使用 git 大约 18 个月了,以为自己已经比较了解 git 了。但当我们请 GitHub 的 Scott Chacon 来 LVS 公司(博彩/游戏软件开发商)做一些培训时,在第一天我学到了很多。
由于一些人总是感觉使用 Git 相当轻松,所以我想分享一些我从社区中学到的精品。或许可以帮助别人不需要做大量的研究而获得答案。
安装 Git 之后,你要做的第一件事情就是配置你的名字和邮箱,因为每一次提交都需要这些信息:
$ git config --global user.name "Some One"
$ git config --global user.email "[email protected]"
存储在 git 中的一切都在一个文件中。当你提交时,git 会建立一个包含你的提交信息和相关数据的文件(名称,邮件,日期/时间,上一次提交等等),并将其链接到一个树形文件。树形文件包含一个对象列表或其他树。对象或二进制大对象是和提交相关的真正内容(如果你愿意,一个文件,虽然文件名没有存储在对象中,但会存储在树中)。所有这些文件都以对象的 SHA-1 哈希值作为文件名来存储。
分支和标签仅仅是包含(基本上)指向提交的 SHA-1 哈希值的文件。使用这些引用会带来很大的灵活性和速度的提升,创建一个新的分支和创建一个带有分支名称和指向提交分支的 SHA-1 哈希值的文件一样简单。当然,你从来不会这样做,你会使用 Git 命令行工具(或 GUI),但它是那么的简单。
你可能听说过 HEAD 引用。这只是一个包含指向你当前提交的 SHA-1 引用的文件。如果你正在解决一个合并冲突问题,查看 HEAD 你会发现,它与特定的分支或分支上的特定点无关,只和你现在的位置有关。
所有的分支指针保存在.git/refs/heads
下,HEAD 在.git/HEAD
下,标签在.git/refs/tags
下 - 你可以随意看看。
当在日志文件中查看一个合并提交的消息时,你会看到两个 parents(与正常一个提交相比)。第一个 parent 是你所在的分支,第二个 parent 是你并入的分支。
到目前为止,我确信你有一个合并冲突并且需要解决。通常通过编辑该文件,删除文件中的 <<<<<,====,>>>> 标记,然后保存你需要保留的代码就可以了。有时候,在任何变更之前查看代码是一个值得推荐的做法,比如,在你对两个有冲突的分支采取行动之前。这是又一个命令:
$ git diff --merge
diff --cc dummy.rb
index 5175dde,0c65895..4a00477
--- a/dummy.rb
+++ b/dummy.rb
@@@ -1,5 -1,5 +1,5 @@@
class MyFoo
def say
- puts "Bonjour"
- puts "Hello world"
++ puts "Annyong Haseyo"
end
end
如果文件是二进制,diff 文件不那么容易……你通常想做的是尝试每个版本的二进制文件并决定使用哪一个(或在二进制文件编辑器手动复制部分内容)。从一个特定分支下 pull 一个文件副本(如果你要合并 master 分支和 132 分支的话):
$ git checkout master flash/foo.fla # 或者...
$ git checkout feature132 flash/foo.fla
$ # 然后...
$ git add flash/foo.fla
另一种方法是从 git 中查看文件——你可以以另一个文件名来查看,然后将正确的文件复制到正常的文件中。
$ git show master:flash/foo.fla > master-foo.fla
$ git show feature132:flash/foo.fla > feature132-foo.fla
# Check out master-foo.fla and feature132-foo.fla
# 比如说,我们决定 feature132 是正确的
$ rm flash/foo.fla
$ mv feature132-foo.fla flash/foo.fla
$ rm master-foo.fla
$ git add flash/foo.fla
更新:感谢 Carl 在原来博客上的评论提醒,你可以使用git checkout — ours flash/foo.fla
和git checkout — theirs flash/foo.fla
来签出 一个特定版本,而无需记住你要合并哪个分支。就我个人而言,我喜欢更加明确,不过选择权在你……
在解决了合并冲突问题之后(就像我上面所做的那样),记得 add 文件。
Git 最强大的功能之一是能够有多个远程服务器(以及你运行一个本地仓库)。你并不总是需要写访问,你可能读取多个服务器(合并工作),然后写入到另一个服务器。添加一个新的远程服务器很简单:
$ git remote add john git@github.com:johnsomeone/someproject.git
如果你想查看远程服务器的信息,你可以这样做:
# 显示每个远程服务器的 URL
$ git remote -v
# 提供更多细节
$ git remote show name
你可以查看本地分支和远程分支之间的差异:
$ git diff master..john/master
你也能查看不在远程分支上的 HEAD 的变化:
$ git log remote/branch.. # Note: no final refspec after ..
在 Git 中有两种类型的标签——轻量级标签和注释标签。记得第二个贴士中说过 Git 是基于指针的,二者的区别很简单。轻量级标签只是一个指向提交的命名指针。你可以改变它指向另一个提交。注释标签是一个指向标签对象的命名指针,这个对象拥有自己的消息和历史。因为它有自己的消息,如果有需要的话,标签对象可以采用 GPG 签名。
创建两种类型的标签很容易(只有一个命令行选项的差异)
$ git tag to-be-tested
$ git tag -a v1.1.0 # Prompts for a tag message
Git 中创建分支是很容易的(闪电般的速度,因为它只需要创建一个小于100字节的文件)。创建一个新的分支,并切换到它的通用方法:
$ git branch feature132
$ git checkout feature132
当然,如果你知道你要马上切换过去,使用一条命令就能做到:
$ git checkout -b feature132
如果你要重命名一个本地分支,同样是件容易的事(长命令行的方式用来显示过程):
$ git checkout -b twitter-experiment feature132
$ git branch -d feature132
更新:或者你可以(像 Brian Palmer 在原来的博客文章的评论中指出的那样)只使用 -m 和 git branch
来一步到位(像 Mike 指出的那样,如果你只给出一个分支,它将会重命名你的当前分支):
$ git branch -m twitter-experiment
$ git branch -m feature132 twitter-experiment
在将来的时候,你想要合并你的变更。有两种方式可以实现:
$ git checkout master
$ git merge feature83 # 或者...
$ git rebase feature83
merge 和 rebase 的区别在于,merge 试图解决变更而且创建一个融合后的新提交,而 rebase 则试图把自你上次在其他分支上的变化,在另一个分支的 HAED 上重现。
但是,你 push 分支到远程服务器后不要进行 rebase——这可能会导致混乱/问题。
如果你不能确定哪些分支仍然有独立的工作在进行——以便你能知道你需要合并哪一个分支以及删除哪些分支,git branch 命令有两个选项可以帮助实现这一点:
# 显示 merge 到你当前分支的所有分支
$ git branch --merged
# 显示没有 merge 到你当前分支的分支
$ git branch --no-merged
如果你有一个本地分支,你希望它出现在远程服务器上,你可以用一个命令来 push:
$ git push origin twitter-experiment:refs/heads/twitter-experiment
# origin 是我们的服务器名,twitter-experiment 是要 push 的分支
更新:感谢 Erlend 在博客文章评论中提到的——这实际上和 git push origin twitter-experiment 达到的效果的一样,但是通过使用全部语法,你能看到你实际上在两端使用了不同的名字(你的本地名字可能是 add-ssl-support,而远程名字可能是 issue-1723)。
如果你想删除一个远程服务器上的分支(注意分支名称前的冒号):
$ git push origin :twitter-experiment
如果你想显示所有远程分支的状态,你可以这样查看它们:
$ git remote show origin
这可能会列出一些服务器上出现过但现在已不存在的分支。如果是这种情况,你可以很使用如下命令从本地轻松地移除它们:
$ git remote prune
最后,如果你希望在本地跟踪远程分支,通用的方法是:
$ git branch --track myfeature origin/myfeature $ git checkout myfeature
然而,如果你使用 -b 选项来 checkout 的话,新版的 Git 会自动建立跟踪:
$ git checkout -b myfeature origin/myfeature
Storing Content in Stashes, Index and File System
在 Git 中你可以将当前的工作区状态存放到临时存储区的栈下,然后重新应用它。简单的例子如下:
$ git stash # Do something...
$ git stash pop
很多人建议使用 git stash apply 代替 pop,但是如果你这样做,你最终得到的一长串无用的存储清单。“pop”只从堆栈彻底移除它。如果你已经使用了 git stash apply,可以使用如下命令从堆栈中删除最后一项:
$ git stash drop
Git 会根据当前提交信息自动创建一个注释。如果您更喜欢使用自定义消息(因为它可能和以前提交没什么关系):
$ git stash save "My stash message"
如果你想从你的列表中应用(不必是最后一个)一个特定的 stash,你可以列出它们并这样使用:
$ git stash list
stash@{0}: On master: Changed to German
stash@{1}: On master: Language is now Italian
$ git stash apply stash@{1}
在 subversion 中你修改文件,然后只提交刚才改变的部分。而在 Git 中你有很多更多的控制权来提交某些文件甚至某些补丁。为了提交某些文件或文件的某些部分,你需要进入交互模式。
$ git add -i
staged unstaged path
*** Commands ***
1: status 2: update 3: revert 4: add untracked
5: patch 6: diff 7: quit 8: help
What now>
这使你进入一个基于交互提示的菜单。你可以使用命令 的数字符合或高亮字符(如果你开启了颜色高亮)进入该模式。然后是输入文件数的问题了(你可以使用如 1 或 1-4 或 2,4,7 的格式)。
如果你想进入 patch 模式(交互模式下输入‘p’或‘5’ ),你也可以直接进入那个模式:
$ git add -p
diff --git a/dummy.rb b/dummy.rb
index 4a00477..f856fb0 100644
--- a/dummy.rb
+++ b/dummy.rb
@@ -1,5 +1,5 @@
class MyFoo
def say
- puts "Annyong Haseyo"
+ puts "Guten Tag"
end
end
Stage this hunk [y,n,q,a,d,/,e,?]?
正如你所看到的,在底部你会得到一组选项,用于选择添加文件的部分更改或文件所有的更改等等。使用’?’命令解释选项的意义。
一些项目(以 Git 项目自身为例)直接在 Git 的文件系统中存储的额外的文件而不必检查。
让我们在 Git 中存储的一个任意的文件:
$ echo "Foo" | git hash-object -w --stdin 51fc03a9bb365fae74fd2bf66517b30bf48020cb
此时的对象是在数据库中,但如果你没有设置指向那个文件的对象,它将被当做垃圾而回收。最简单的方法是标记它:
$ git tag myfile 51fc03a9bb365fae74fd2bf66517b30bf48020cb
注意,当我们需要得到文件时,可以使用tag myfile
来做到这一点:
$ git cat-file blob myfile
对于开发人员使用工具文件(密码,GPG密钥等)可能是有用的,使你不必每次都 check out 到磁盘(特别是在生产环境中)。
不使用“git log”查看最新提交历史的话,你将不能长久使用 Git。不过,也有关于如何更好使用它的贴士。例如,您可以查看每个 commit 的 patch 更改内容:
$ git log -p
或者查看文件更改的摘要:
$ git log --stat
你可以在单行中设置一个不错的别名,用来显示简短的提交和带有消息的漂亮分支图(像 gitk,但在命令行上):
$ git config --global alias.lol "log --pretty=oneline --abbrev-commit --graph --decorate"
$ git lol
* 4d2409a (master) Oops, meant that to be in Korean
* 169b845 Hello world
如果你要在日志中搜索一个特定作者,可以这样指定:
$ git log --author=Andy
更新:感谢 Johannes 的评论,我清理了一部分困惑。
或者,如果你有一个出现在提交信息中的搜索词:
$ git log --grep="Something in the message"
还有一个功能更强大的叫 pickaxe 的命令,它可以查找添加或删除一个特定的内容的条目(即,当它第一次出现或已被删除)。这样你就可以知道何时增加了一行(但是如果那一行中的字符随后被改变,你将无从得知):
$ git log -S "TODO: Check for admin status"
如果更改一个特定的文件会怎样,比如lib/foo.rb
$ git log lib/foo.rb
比方说,你有一个 feature/132 分支和 feature/145 分支,你想要查看在这些分支而不在 master 分支的提交(注意:^意思是非):
$ git log feature/132 feature/145 ^master
你也可以使用 ActiveSupport 风格的日期来缩小日期范围:
$ git log --since=2.months.ago --until=1.day.ago
默认情况下它会使用 OR 来组合查询,但你可以轻松将其更改为使用 AND 来查询(如果你有一个以上的准则)
$ git log --since=2.months.ago --until=1.day.ago --author=andy -S "something" --all-match
当引用一个版本的时候,有许多选项可以选择,这取决于你的了解程度:
$ git show 12a86bc38 # By revision
$ git show v1.0.1 # By tag
$ git show feature132 # By branch name
$ git show 12a86bc38^ # Parent of a commit
$ git show 12a86bc38~2 # Grandparent of a commit
$ git show feature132@{yesterday} # Time relative
$ git show feature132@{2.hours.ago} # Time relative
需要注意的是,和在上一节有所不同,行尾的 ^ 意味着是 parent 的提交——行首的 ^ 表示不在这个分支上。
最简单的使用方法:
$ git log origin/master..new
# [old]..[new] - everything you haven't pushed yet
You can also omit the [new] and it will use your current HEAD.
如果你还没有提交更改,你可以很容易地重置它:
$ git reset HEAD lib/foo.rb
通常使用‘unstage’作为别名,因为它不是那么显而易见。
$ git config --global alias.unstage "reset HEAD"
$ git unstage lib/foo.rb
如果你已经提交了文件,你可以做两件事情——如果是最后的提交,你可以这样修改它:
$ git commit --amend
这将撤销最后一次提交,使你的工作区回到暂存区变化前的状态,你可以编辑/提交消息准备下一次提交。
如果你已经提交不止一次,并想彻底撤销修改,你可以重置分支回到之前的时间点。
$ git checkout feature132 $ git reset --hard HEAD~2
如果你真的想把分支指向一个完全不同的 SHA-1(也许你把一个分支的 HEAD 指向另一个分支,或者进一步提交),你可以按下面方式来做:
$ git checkout FOO
$ git reset --hard SHA
实际上还有一种更快的方式(因为它不会先将你的工作区变回最初 FOO 状态然后再指向 SHA):
$ git update-ref refs/heads/FOO SHA
好吧,让我们假设你提交到主分支,但已经创建了一个叫做 experimental 的主分支。为了移除这些变化,你可以在当前点创建一个分支,回退 HEAD,然后 checkout 新的分支:
$ git branch experimental
# 建立一个指向当前主分支的指针
$ git reset --hard master~3
# 移除主分支指针回退到3个版本之前
$ git checkout experimental
如果你已经在一个分支的分支的分支上面做了些变更,这将会更复杂。你需要做的就是在这个分支上 rebase 变更到另一个的地方:
$ git branch newtopic STARTPOINT
$ git rebase oldtopic --onto newtopic
这是一个很酷的功能,我之前已看过演示,但从没有真正明白,现在来看其实很简单。比方说,你已做了3次提交,但是你想对它们重新排序或者编辑(或者合并它们):
$ git rebase -i master~3
然后带一些指令打开编辑器。你所要做的就是修改 “pick/squash/edit 的指令来进行如何提交,然后保存/退出。在编辑之后,你可以使用 git rebase –continue 让你的指令一个一个进行。
如果你选择编辑一个文件,这会让你停留在你提交时的状态,因此你需要使用 git commit –amend 来编辑它。
注意:在 REBASE 过程中不要进行提交——只能添加然后使用--continue
, --skip
或者--abort
。
如果你已经提交了一些内容到分支中(也许你是从SVN中的旧代码库导入的),你删除掉历史记录中所有的已提交内容:
$ git filter-branch --tree-filter 'rm -f *.class' HEAD
如果你已经向远程服务器推送过代码,但自那之后提交的都是一些垃圾,在推送之前你可以在本地系统上执行这样的操作:
$ git filter-branch --tree-filter 'rm -f *.class' origin/master..HEAD
如果你知道你之前已经查看过一个 SHA-1,但是你已经做了一些重置/回退工作,你可以使用 reflog 命令去查看你最近看过的 SHA-1:
$ git reflog
$ git log -g # Same as above, but shows in 'log' format
一个可爱的小技巧——不要忘记,分支名称不限于 A-Z 和 0-9。使用 / 和 . 来伪装命名空间或版本号是相当不错的,例如:
$ # Generate a changelog of Release 132
$ git shortlog release/132 ^release/131
$ # Tag this as v1.0.1
$ git tag v1.0.1 release/132
通常,找出是谁在文件中改变了代码是很有用的。简单的命令来做到这一点:
$ git blame FILE
有时更改来自于以前文件(如果你已经合并了两个文件,或者你移动了一个函数),因此你可以这样用:
$ # shows which file names the content came from
$ git blame -C FILE
有时通过向前或向后点击来进行变化跟踪是很好的方法。有个不错的内置的 GUI 程序能做到这点:
$ git gui blame FILE
Git 通常不需要大量维护,它基本上可以自我维护。然而,你可以查看数据库统计信息:
$ git count-objects -v
如果数值很高,你可以选择使用垃圾回收你的复制内容。这不会影响推送或其它用户,但却可以让你的命令运行更快且占用更少空间:
$ git gc
每隔一段时间运行一致性检查也是值得的:
$ git fsck --full
你也可以在行尾添加一个 –auto 参数(如果你频繁或每天在你的服务器上从 crontab 中运行它),如果统计数据表明需要进行要进行一致性检查,只要 fsck 命令就行。
如果检查时得到 “dangling” 或 “unreachable” 的结果是正常的,这往往是由于回退 HEAD 或 rebasing 的结果。如果得到“missing” 或 “sha1 mismatch” 则表示出了问题……寻求专业帮助吧!
如果你使用 -D 选项删除了一个分支 experimental,你可以重建它:
$ git branch experimental SHA1_OF_HASH
你可以使用 git reflog 找到 SHA-1 哈希值,如果你最近访问过它的话。
另一种方法是使用git fsck —lost-found
。一个 dangling 提交就是一个 lost HEAD(它只会是一个已删除分支的 HEAD,因为当一个 HEAD^ 被 HEAD 引用时,它就不会 dangling)
完成!