《Pro Git》读书笔记
选择修改版本
Git 允许你通过几种方法来指明特定的或者一定范围内的提交。
单个修订版本
Git 十分智能,你只需要提供 SHA-1 的前几个字符就可以获得对应的那次提交,当然你提供的 SHA-1 字符数量不得少于 4 个,并且没有歧义
分支引用
指明一次提交最直接的方法是有一个指向它的分支引用。这样你就可以在任意一个 Git 命令中使用这个分支名来代替对应的提交对象或者 SHA-1 值。
引用日志
当你在工作时, Git 会在后台保存一个引用日志(reflog),引用日志记录了最近几个月你的 HEAD 和分支引用所指向的历史。
你可以使用 git reflog 来查看引用日志
详情可以查看这里
祖先引用
祖先引用是另一种指明一个提交的方式。如果你在引用的尾部加上一个 ^, Git 会将其解析为该引用的上一个提交。
另一种指明祖先提交的方法是 ~。同样是指向第一父提交,因此 HEAD~ 和 HEAD^ 是等价的
提交区间
最常用的指明提交区间语法是双点。这种语法可以让 Git 选出在一个分支中而不在另一个分支中的提交。
你想要查看 experiment 分支中还有哪些提交尚未被合并入 master 分支。你可以使用 master..experiment来让 Git 显示这些提交。1
git log master..experiment
双点语法很好用,但有时候你可能需要两个以上的分支才能确定你所需要的修订,比如查看哪些提交是被包含在某些分支中的一个,但是不在你当前的分支上。你想查所有被refA 或 refB 包含的但是不被 refC 包含的提交,你可以输入下面中的任意一个命令1
2$ git log refA refB ^refC
$ git log refA refB --not refC
交互式暂存
本节的几个互交命令可以帮助你将文件的特定部分组合成提交。当你修改一组文件后,希望这些改动能放到若干提交而不是混杂在一起成为一个提交时,这几个工具会非常有用。
通过这种方式,可以确保提交是逻辑上独立的变更集,同时也会使其他开发者在与你工作时很容易地审核。如果运行 git add 时使用 -i 或者 –interactive 选项,Git 将会进入一个交互式终端模式
储藏与清理
有时,当你在项目的一部分上已经工作一段时间后,所有东西都进入了混乱的状态,而这时你想要切换到另一个分支做一点别的事情。问题是,你不想仅仅因为过会儿回到这一点而为做了一半的工作创建一次提交。针对这个问题的答案是 git stash 命令。
储藏工作
- 将新的储藏推送栈上:
git stash
或者git stash save
- 查看栈上的储藏:
git stash list
- 将栈顶的储藏应用:
git stash apply
- 丢弃指定储藏
git stash drop {name}
- 应用栈顶储藏并弹出
git stash pop
创造性储藏
有几个储藏的变种可能也很有用。第一个非常流行的选项是 stash save
命令的 --keep-index
选项。它告诉Git 不要储藏任何你通过 git add 命令已暂存的东西。
另一个经常使用储藏来做的事情是像储藏跟踪文件一样储藏未跟踪文件。默认情况下,git stash 只会储藏已经在索引中的文件。如果指定 --include-untracked
或 -u 标记,Git 也会储藏任何创建的未跟踪文件。
从储藏创建一个分支
如果储藏了一些工作,将它留在那儿了一会儿,然后继续在储藏的分支上工作,在重新应用工作时可能会有问题。如果应用尝试修改刚刚修改的文件,你会得到一个合并冲突并不得不解决它。如果想要一个轻松的方式来再次测试储藏的改动,可以运行 git stash branch
创建一个新分支,检出储藏工作时所在的提交,重新在那应用工作,然后在应用成功后扔掉储藏
清理工作目录
对于工作目录中一些工作或文件,你想做的也许不是储藏而是移除。git clean 命令会帮你做这些事
签署工作
Git 虽然是密码级安全的,但它不是万无一失的。如果你从因特网上的其他人那里拿取工作,并且想要验证提交是不是真正地来自于可信来源,Git 提供了几种通过 GPG 来签署和验证工作的方式。
搜索
无论仓库里的代码量有多少,你经常需要查找一个函数是在哪里调用或者定义的,或者一个方法的变更历史。Git 提供了两个有用的工具来快速地从它的数据库中浏览代码和提交。
Git Grep
Git 提供了一个 grep 命令,你可以很方便地从提交历史或者工作目录中查找一个字符串或者正则表达式。
默认情况下 Git 会查找你工作目录的文件。你可以传入 -n 参数来输出 Git 所找到的匹配行行号。你可以使用 –count 选项来使 Git 输出概述的信息,仅仅包括哪些文件包含匹配以及每个文件包含了多少个匹配。如果你想看匹配的行是属于哪一个方法或者函数,你可以传入 -p 选项
1 | $ git grep -n gmtime_r |
Git 日志搜索
或许你不想知道某一项在 哪里 ,而是想知道是什么 时候 存在或者引入的。git log 命令有许多强大的工具可以通过提交信息甚至是 diff 的内容来找到某个特定的提交。
例如,如果我们想找到 ZLIB_BUF_MAX 常量是什么时候引入的,我们可以使用 -S 选项来显示新增和删除该字符串的提交。1
2
3$ git log -SZLIB_BUF_MAX --oneline
e01503b zlib: allow feeding more than 4GB in one go
ef49a7a zlib: zlib can only process 4GB at a time
重写历史
许多时候,在使用 Git 时,可能会因为某些原因想要修正提交历史。Git 很棒的一点是它允许你在最后时刻做决定。你可以在将暂存区内容提交前决定哪些文件进入提交,可以通过 stash 命令来决定不与某些内容工作,也可以重写已经发生的提交就像它们以另一种方式发生的一样。
修改最后一次提交
修改你最近一次提交可能是所有修改历史提交的操作中最常见的一个。对于你的最近一次提交,你往往想做两件事情:修改提交信息,或者修改你添加、修改和移除的文件的快照。
如果,你只是想修改最近一次提交的提交信息,那么很简单:git commit --amend
修改多个提交信息
Git 没有一个改变历史工具,但是可以使用变基工具来变基一系列提交,基于它们原来的 HEAD 而不是将其移动到另一个新的上面。可以通过给 git rebase 增加 -i选项来交互式地运行变基。git rebase -i HEAD~3
重新排序提交
修改交互式变基中的各个提交的顺序即可
压缩提交
通过交互式变基工具,也可以将一连串提交压缩成一个单独的提交。在变基信息中脚本给出了有用的指令:1
2
3
4
5
6
7# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
核武器级选项:filter-branch
有另一个历史改写的选项,如果想要通过脚本的方式改写大量提交的话可以使用它 - 例如,全局修改你的邮箱地址或从每一个提交中移除一个文件。这个命令是 filter-branch,它可以改写历史中大量的提交,除非你的项目还没有公开并且其他人没有基于要改写的工作的提交做的工作,你不应当使用它。
从每一个提交移除一个文件
这经常发生。有人粗心地通过 git add . 提交了一个巨大的二进制文件,你想要从所有地方删除它。然而你想要开源项目。filter-branch 是一个可能会用来擦洗整个提交历史的工具。为了从整个提交历史中移除一个叫做 passwords.txt 的文件,可以使用 –tree-filter 选项给filter-branch:1
git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
--tree-filter
选项在检出项目的每一个提交后运行指定的命令然后重新提交结果。
使一个子目录做为新的根目录
假设已经从另一个源代码控制系统中导入,并且有几个没意义的子目录(trunk、tags 等等)。如果想要让trunk 子目录作为每一个提交的新的项目根目录,filter-branch 也可以帮助你那么做:1
git filter-branch --subdirectory-filter trunk HEAD
重置揭秘
这里将讨论reset与checkout
三棵树
理解 reset 和 checkout 的最简方法,就是以 Git 的思维框架(将其作为内容管理器)来管理三棵不同的树。“树” 在我们这里的实际意思是 “文件的集合”,而不是指特定的数据结构
Git 作为一个系统,是以它的一般操作来管理并操纵这三棵树的:
树 | 用途 |
---|---|
HEAD | 上一次提交的快照,下一次提交的父结点 |
Index | 预期的下一次提交的快照 |
Working Directory | 沙盒 |
HEAD
HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。这表示 HEAD 将是下一次提交的父结点。通常,理解 HEAD 的最简方式,就是将它看做 你的上一次提交 的快照。
Index
索引是你的 预期的下一次提交。我们也会将这个概念引用为 Git 的 “暂存区域”,这就是当你运行 git commit 时 Git 看起来的样子。
工作流程
参考这里
重置的作用(reset)
- 第一步:移动HEAD
- 第二步:更新索引(–mixed)
- 第三步:更新工作目录(–hard)
reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止:
- 移动 HEAD 分支的指向 (若指定了 –soft,则到此停止)
- 使索引看起来像 HEAD (默认执行到这一步)
- 使工作目录看起来像索引 (指定了 –hard ,才执行这一步)
通过路径来重置
若指定了一个路径,reset 将会跳过第 1 步,并且将它的作用范围限定为指定的文件或文件集合。
压缩
我们来看看如何利用这种新的功能来做一些有趣的事情 - 压缩提交
1 | git reset --soft HEAD~2 |
检出(checkout)
运行 git checkout [branch]
与运行git reset --hard [branch]
非常相似,它会更新所有三棵树使其看起来像 [branch],不过有两点重要的区别。
首先不同于 reset –hard,checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件吹走。其实它还更聪明一些。它会在工作目录中先试着简单合并一下,这样所有还未修改过的文件都会被更新。而 reset –hard 则会不做检查就全面地替换所有东西。
第二个重要的区别是如何更新 HEAD。reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。
带路径情况
运行 checkout 的另一种方式就是指定一个文件路径,这会像 reset 一样不会移动 HEAD。它就像是 git reset --hard [branch] file
(如果 reset 允许你这样运行的话)- 这样对工作目录并不安全,它也不会移动 HEAD。
小结
高级合并
Git 并不会尝试过于聪明的合并冲突解决方案。Git的哲学是聪明地决定无歧义的合并方案,但是如果有冲突,它不会尝试智能地自动解决它。
合并冲突
首先,在做一次可能有冲突的合并前尽可能保证工作目录是干净的。
在遇到冲突的时候,你可以有以下几种解决方案:
中断一次合并
你可能不想处理冲突这种情况,完全可以通过 git merge --abort
来简单地退出合并。他会会尝试恢复到你运行合并前的状态。但当运行命令前,在工作目录中有未储藏、未提交的修改时它不能完美处理,除此之外它都工作地很好。
忽略空白
如果你看到在一次合并中有大量的空白问题,你可以简单地中止它并重做一次,这次使用-Xignore-all-space
或 -Xignore-space-change
选项。第一个选项忽略任意 数量 的已有空白的修改,第二个选项忽略所有空白修改。1
git merge -Xignore-space-change whitespace
手动文件再合并
首先,我们进入到了合并冲突状态。然后我们想要我的版本的文件,他们的版本的文件(从我们将要合并入的分支)和共同的版本的文件(从分支叉开时的位置)的拷贝。
然后我们想要修复任何一边的文件,并且为这个单独的文件重试一次合并。获得这三个文件版本实际上相当容易。Git 在索引中存储了所有这些版本,在 “stages” 下每一个都有一个数字与它们关联。Stage 1 是它们共同的祖先版本,stage 2 是你的版本,stage 3 来自于 MERGE_HEAD,即你将要合并入的版本(“theirs”)
如果你想要在最终提交前看一下我们这边与另一边之间实际的修改,你可以使用 git diff
要在合并前比较结果与在你的分支上的内容,换一句话说,看看合并引入了什么,可以运行 git diff --ours
; 如果我们想要查看合并的结果与他们那边有什么不同,可以运行 git diff --theirs
子树合并
子树合并的思想是你有两个项目,并且其中一个映射到另一个项目的一个子目录,或者反过来也行。
我们希望将 Rack 项目拉到 master 项目中作为一个子目录。我们可以在 Git 中执行 git read-tree
来实现。它会读取一个分支的根目录树到当前的暂存区和工作目录里。先切回你的 master 分支,将 rack_back 分支拉取到我们项目的 master 分支中的 rack 子目录。1
git read-tree --prefix=rack/ -u rack_branch
当 Rack 项目有更新时,我们可以切换到那个分支来拉取上游的变更。1
2git checkout rack_branch
git pull
接着,我们可以将这些变更合并回我们的 master 分支。使用 –squash 选项和使用 -Xsubtree 选项(它采用递归合并策略),都可以用来可以拉取变更并且预填充提交信息。1
2git checkout master
git merge --squash -s recursive -Xsubtree=rack rack_branch
Rerere
git rerere 功能是一个隐藏的功能。正如它的名字 “reuse recorded resolution” 所指,它允许你让 Git 记住解决一个块冲突的方法,这样在下一次看到相同冲突时,Git 可以为你自动地解决它。
有几种情形下这个功能会非常有用。在文档中提到的一个例子是如果你想要保证一个长期分支会干净地合并,但是又不想要一串中间的合并提交。将 rerere 功能打开后偶尔合并,解决冲突,然后返回到合并前。如果你持续这样做,那么最终的合并会很容易,因为 rerere 可以为你自动做所有的事情。
可以将同样的策略用在维持一个变基的分支时,这样就不用每次解决同样的变基冲突了。或者你将一个分支合并并修复了一堆冲突后想要用变基来替代合并 - 你可能并不想要再次解决相同的冲突。
另一个情形是当你偶尔将一堆正在改进的特性分支合并到一个可测试的头时,就像 Git 项目自身经常做的。如果测试失败,你可以倒回合并之前然后在去除导致测试失败的那个特性分支后重做合并,而不用再次重新解决所有的冲突。
启用rerere功能的方法:
git config --global rerere.enabled true
- 在特定的仓库中创建
.git/rr-cache
目录
更多使用方法可以查看这里
Git 调试
Git 提供两个工具来辅助你调试项目中的问题
文件标注
如果你在追踪代码中的一个 bug,并且想知道是什么时候以及为何会引入,文件标注通常是最好用的工具。它展示了文件中每一行最后一次修改的提交。所以,如果你在代码中看到一个有问题的方法,你可以使用 gitblame 标注这个文件,查看这个方法每一行的最后修改时间以及是被谁修改的。这个例子使用 -L 选项来限制输出范围在第12至22行:
1 | git blame -L 12,22 simplegit.rb |
二分查找
当你知道问题是在哪里引入的情况下文件标注可以帮助你查找问题。如果你不知道哪里出了问题,并且自从上次可以正常运行到现在已经有数十个或者上百个提交,这个时候你可以使用 git bisect 来帮助查找。bisect命令会对你的提交历史进行二分查找来帮助你尽快找到是哪一个提交引入了问题。
首先执行 git bisect start
来启动,接着执行git bisect bad
来告诉系统当前你所在的提交是有问题的。然后你必须告诉 bisect 已知的最后一次正常状态是哪次提交,使用git bisect good [good_commit]
在测试过程中可以通过 git bisect good
来告诉 Git 当前的提交没有问题,通过git bisect bad
来告诉Git当前的提交有问题
子模块
子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
开始使用子模块
你可以通过在 git submodule add 命令后面加上想要跟踪的项目 URL 来添加新的子模块。在本例中,我们将会添加一个名为 “DbConnector” 的库。1
git submodule add https://github.com/chaconinc/DbConnector
Git首先会生成新的 .gitmodules
文件。该置文件保存了项目 URL 与已经拉取的本地目录之间的映射。如果有多个子模块,该文件中就会有多条记录。要重点注意的是,该文件也像 .gitignore 文件一样受到(通过)版本控制。它会和该项目的其他部分一同被拉取推送。
克隆含有子模块的项目
你必须运行两个命令:git submodule init
用来初始化本地配置文件,而 git submodule update
则从该项目中抓取所有数据并检出父项目中列出的合适的提交。
如果给 git clone 命令传递 –recursive 选项,它就会自动初始化并更新仓库中的每一个子模块。
在包含子模块的项目上工作
在项目中使用子模块的最简模型,就是只使用子项目并不时地获取更新,而并不在你的检出中进行任何更改。
当我们运行 git submodule update 从子模块仓库中抓取修改时,Git 将会获得这些改动并更新子目录中的文件,但是会将子仓库留在一个称作 “游离的 HEAD” 的状态。
打包
Git 可以将它的数据 “打包” 到一个文件中。这在许多场景中都很有用。有可能你的网络中断了,但你又希望将你的提交传给你的合作者们。可能你不在办公网中并且出于安全考虑没有给你接入内网的权限。
bundle 命令会将 git push 命令所传输的所有内容打包成一个二进制文件,你可以将这个文件通过邮件或者闪存传给其他人,然后解包到其他的仓库中。
如果你想把这个仓库发送给其他人但你没有其他仓库的权限,或者就是懒得新建一个仓库,你就可以用 gitbundle create 命令来打包1
git bundle create repo.bundle HEAD master
然后你就会有一个名为 repo.bundle 的文件,该文件包含了所有重建该仓库 master 分支所需的数据。
假设别人传给你一个 repo.bundle 文件并希望你在这个项目上工作。你可以从这个二进制文件中克隆出一个目录,就像从一个 URL 克隆一样。1
git clone repo.bundle repo
替换
Git 对象是不可改变的,但它提供一种有趣的方式来用其他对象假装替换数据库中的 Git 对象。
replace
命令可以让你在 Git 中指定一个对象并可以声称“每次你遇到这个 Git 对象时,假装它是其他的东西”。在你用一个不同的提交替换历史中的一个提交时,这会非常有用。