ProGit 读书笔记

Table of Contents

  • 一、记录每次更新到仓库
    * 移除文件
  • 二、查看提交历史
    * 限制输出长度
  • 三、撤消操作
    * 取消暂存的文件
    * 撤消对文件的修改
  • 四、远程仓库的使用
    * 查看远程仓库
    * 添加远程仓库
    * 从远程仓库中抓取与拉取
  • 五、打标签
    * 列出标签
    * 附注标签
    * 轻量标签
    * 后期打标签
    * 共享标签
    * 检出标签
  • 六、分支的新建与合并
    * 新建分支
    * 合并分支
  • 七、分支管理
  • 八、远程分支
    * 推送
    * 跟踪分支
    * 拉取
    * 删除远程分支
  • 九、变基
    * 变基的基本操作
    * 更有趣的变基例子
    * 变基的风险 & 用变基解决变基
  • 十、分布式工作流程
  • 十一、向一个项目贡献
    * 提交准则
    * 私有管理团队
    * 派生的公开项目
  • 十二、选择修订版本
    * 引用日志
    * 祖先引用
    * 提交区间
    * 多点
    * 三点
  • 十三、交互式暂存
  • 十四、储藏与清理
    * 储藏工作
    * 创造性的储藏
    * 从储藏创建一个分支
  • 十五、搜索
    * Git Grep
  • 十六、重写历史
    * 修改最后一次提交
    * 修改多个提交信息
    * 重新排序提交
    * 压缩提交
    * 拆分提交
    * 核武器级选项:filter-branch
    * 从每一个提交移除一个文件
    * 全局修改邮箱地址
  • 十七、重置揭密
    * 三棵树 HEAD, Index, Working Directory
    * 重置的作用
    * 通过路径来重置
    * 压缩
    * 检出
    * 总结
  • 十八、高级合并
    * 合并冲突
  • 十九、使用 Git 调试
    * 文件标注
    * 二分查找
  • 二十、配置 Git
    * 外部的合并与比较工具
    * 格式化与多余的空白字符
  • 二十一、Git 属性
    * 合并策略
  • 二十二、Git 钩子
    * 客户端钩子
  • 二十三、Git 对象
    * 树对象
    * 提交对象
  • 二十四、Git 引用
    * HEAD 引用
  • 二十五、维护与数据恢复
    * 数据恢复
    * 移除对象

一、记录每次更新到仓库

移除文件

  1. 要从 Git 中移除某个文件,就必须要从暂存区域移除,然后提交。可以用 git rm 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中了。

  2. 如果删除之前修改过并且已经放到暂存区域的话,git rm 必须要用强制删除选项 -f

  3. 如果想让文件保留在磁盘,但是并不想让 Git 继续跟踪。当你忘记添加 .gitignore 文件,不小心把一个很大的日志文件或一堆 .a 这样的编译生成文件添加到暂存区时,使用 --cached 选项:

    $ git rm --cached README
    
  4. 要在 Git 中对文件改名,可以使用 Git 的 mv 命令

    $ git mv file_from file_to
    

二、查看提交历史

限制输出长度

  1. git log--author 选项显示指定作者的提交,用 --grep 选项搜索提交说明中的关键字。(请注意,如果要得到同时满足这两个选项搜索条件的提交,就必须用 --all-match 选项。否则,满足任意一个条件的提交都会被匹配出来)

  2. 另一个非常有用的筛选选项是 -S,可以列出那些添加或移除了某些字符串的提交。比如说,你想找出添加或移除了某一个特定函数的引用的提交,你可以这样使用:

    $ git log -Sfunction_name
    
  3. 限制 git log 输出的选项

    限制 git log 输出的选项

三、撤消操作

  • 提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。此时,可以运行带有 --amend 选项的提交命令尝试重新提交:

    $ git commit --amend
    

取消暂存的文件

  • 使用 git reset HEAD <file>... 来取消暂存

撤消对文件的修改

  1. 使用 git checkout -- <file>..." 来撤销对文件的修改

  2. 你需要知道 git checkout -- [file] 是一个危险的命令,这很重要。你对那个文件做的任何修改都会消失 - 你只是拷贝了另一个文件来覆盖它

四、远程仓库的使用

查看远程仓库

  1. git remote -v 会显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL

    image

  2. 想要查看某一个远程仓库的更多信息,可以使用 git remote show [remote-name] 命令。它会列出远程仓库的 URL 与跟踪分支的信息。这些信息非常有用,它告诉你正处于 master 分支,并且如果运行 git pull,就会抓取所有的远程引用,然后将远程 master 分支合并到本地 master 分支

    image

    这个命令会列出当你在特定的分支上执行 git push 会自动地推送到哪一个远程分支。它也会列出哪些远程分支不在你的本地,哪些远程分支已经从服务器上移除了,还有当你执行 git pull 时哪些分支会自动合并

  3. 如果因为一些原因想要移除一个远程仓库 - 你已经从服务器上搬走了或不再想使用某一个特定的镜像了,又或者某一个贡献者不再贡献了 - 可以使用 git remote rm <remote_name>

添加远程仓库

  • 运行 git remote add <shortname> <url> 添加一个新的远程 Git 仓库

从远程仓库中抓取与拉取

  1. 如果你想拉取 Paul 的仓库中有但你没有的信息,可以运行 git fetch pb

  2. 如果你有一个分支设置为跟踪一个远程分支,可以使用 git pull 命令来自动的抓取然后合并远程分支到当前分支。默认情况下,git clone 命令会自动设置本地 master 分支跟踪克隆的远程仓库的 master 分支(或不管是什么名字的默认分支)。运行 git pull 通常会从最初克隆的服务器上抓取数据并自动尝试合并到当前所在的分支。

  3. 另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。又或者你可以自己手动完成这个过程,先 git fetch,再 git rebase

  4. 如果你习惯使用 git pull ,同时又希望默认使用选项 --rebase,你可以执行这条语句 git config --global pull.rebase true 来更改 pull.rebase 的默认配置。

    image

五、打标签

  • Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)

列出标签

  • 列出已有的标签是非常简单直观的。只需要输入 git tag

    $ git tag 
    v0.1 
    v1.3
    

附注标签

  1. 创建一个附注标签最简单的方式是当你在运行 tag 命令时指定 -a 选项:

    $ git tag -a v1.4 -m 'my version 1.4' 
    $ git tag 
    v0.1
    v1.3
    v1.4
    
  2. git show 命令可以看到标签信息与对应的提交信息

轻量标签

  • 轻量标签本质上是将提交校验和存储到一个文件中 - 没有保存任何其他信息。创建轻量标签,不需要使用 -a-s-m 选项,只需要提供标签名字:

    $ git tag v1.5
    $ git tag
    v0.1 
    v1.3 
    v1.4 
    v1.5
    

后期打标签

  • 假设在 v1.2 时你忘记给项目打标签,也就是在 “updated rakefile” 提交。你可以在之后补上标签。要在哪个提交上打标签,你需要在命令末尾指定提交的校验和:

    $ git tag -a v1.2 9fceb02
    

共享标签

  1. 默认情况下,git push 命令并不会传送标签到远程仓库服务器上。在创建完标签后你必须显式地推送标签到共 享服务器上。

    git push origin [tag_name]
    
  2. 如果想要一次性推送很多标签,也可以使用带有 git push origin --tags,这将会把所有不在远程仓库服务器上的标签全部推送。

检出标签

  • 在 Git 中你并不能真的检出一个标签,因为它们并不能像分支一样来回移动。如果你想要工作目录与仓库中特定的标签版本完全一样,可以使用 git checkout -b [branchname] [tagname]在特定的标签上创建一个新分支

    $ git checkout -b version2 v2.0.0 
    Switched to a new branch 'version2'
    

六、分支的新建与合并

新建分支

  1. 在你切换分支之前,保持好一个干净的状态。有一些方法:保存进度(stashing) 和 修补提交(commit amending))

  2. 在合并的时候,你应该注意到了"快进(fast-forward)"这个词。由于当前 master 分支所指向的提交是你当前提交的直接上游,所以 Git 只是简单的将指针向前移动。换句话说,当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候,只会简单的将指针向前推进 (指针右移),因为这种情况下的合并操作没有需要解决的分歧。

合并分支

  1. 你的开发历史从一个更早的地方开始分叉开来(diverged)。因为,master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并。 高亮 [57]

  2. Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。这个被称作一次合并提交,它的特别之处在于他有不止一个父提交 高亮 [58]

七、分支管理

  1. 如果需要查看每一个分支的最后一次提交,可以运行 git branch -v 命令

      $ git branch -v 
      iss53 93b412c fix javascript issue 
    
  • master 7a98805 Merge branch 'iss53'
    testing 782fd34 add scott to the author list in the readmes
    
    
  1. 如果分支包含了还未合并的工作,尝试使用 git branch -d 命令删除它时会失败,如果真的想要删除分支并丢掉那些工作,可以使用 -D 选项强制删除。

八、远程分支

  1. 远程跟踪分支是远程分支状态的引用。它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。

  2. 如果你在本地的 master 分支做了一些工作,然而在同一时间,其他人推送提交到 git.ourcompany.com 并更新了它的 master 分支,那么你的提交历史将向不同的方向前进。也许,只要你不与 origin 服务器连接,你的 origin/master 指针就不会移动

  3. 如果要同步你的工作,运行 git fetch origin 命令。这个命令查找 “origin” 是哪一个服务器(在本例中,它是 git.ourcompany.com),从中抓取本地没有的数据,并且更新本地数据库,移动 origin/master 指针指向新的、更新后的位置

推送

  1. 当你想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。本地的分支并不会自动与远程仓库同步 - 你必须显式地推送想要分享的分支。这样,你就可以把不愿意分享的内容放到私人分支上,而将需要和别人协作的内容推送到公开分支。

  2. 你也可以运行 git push origin serverfix:serverfix,它会做同样的事 - 相当于它说,“推送本地 的 serverfix 分支,将其作为远程仓库的 serverfix 分支”。
    可以通过这种格式来推送本地分支到一个命名不相同的远程分支。如果并不想让远程仓库上的分支叫做 serverfix,可以运行 git push origin serverfix:awesomebranch 来将本地的 serverfix 分支推送到远程仓库上的 awesomebranch 分支。

  3. 要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。即是说,这种情况下,不会有一个新的 serverfix 分支 - 只有一个不可以修改的 origin/serverfix 指针。

  4. 可以运行 git merge origin/serverfix 将这些工作合并到当前所在的分支。如果想要在自己的 serverfix 分支上工作,可以将其建立在远程跟踪分支之上:

    $ git checkout -b serverfix origin/serverfix
    

跟踪分支

  1. 从一个远程跟踪分支检出一个本地分支会自动创建一个叫做 “跟踪分支”(有时候也叫做 “上游分支”)。跟踪分支是与远程分支有直接关系的本地分支。如果在一个跟踪分支上输入 git pull,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。

  2. 最简单的就是,运行 git checkout -b [branch] [remotename]/[branch]。这是一个十分常用的操作,所以 Git 提 供了 --track 快捷方式:

    $ git checkout --track origin/serverfix
    Branch serverfix set up to track remote branch serverfix from origin. 
    Switched to a new branch 'serverfix'
    
  3. 如果想要将本地分支与远程分支设置为不同名字,你可以轻松地增加一个不同名字的本地分支:

    $ git checkout -b sf origin/serverfix
    
  4. 设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,你可以在任意时间使用 -u 或 --set-upstream-to 选项运行 git branch 来显式地设置。

    $ git branch -u origin/serverfix
    
  5. 当设置好跟踪分支后,可以通过 @{upstream}@{u} 快捷方式来引用它。

  6. 如果想要查看设置的所有跟踪分支,可以使用 git branch-vv 选项。

      $ git branch -vv 
      iss53 7e424c3 [origin/iss53: ahead 2] forgot the brackets 
      master 1ae2a45 [origin/master] deploying index fix 
    * serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
    

    如果想要统计最新的领先与落后数字,需要在运行此命令前抓取 所有的远程仓库。可以像这样做:$ git fetch --all; git branch -vv

拉取

  • git pull 在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。

删除远程分支

  • 可以运行带有 --delete 选项的 git push 命令来删除一个远程分支。如果想要从服务器上删除 serverfix 分支,运行下面的命令:

    $ git push origin --delete serverfix
    To https://github.com/schacon/simplegit
    - [deleted] serverfix
    

九、变基

变基的基本操作

  1. 有一种方法:你可以提取在 C4 中引入的补丁和修改,然后在 C3 的基础上再应用一次。在 Git 中,这种操作就叫做 变基。你可以使用 rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。 在上面这个例子中,运行:

    $ git checkout experiment 
    $ git rebase master
    First, rewinding head to replay your work on top of it...
     Applying: added staged command 
    

    它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。 高亮 [80]

  2. 无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

更有趣的变基例子

  1. 假设你希望将 client 中的修改合并到主分支并发布,但暂时并不想合并 server 中的修改,因为它们还需要经过更全面的测试。这时,你就可以使用 git rebase 命令的 --onto 选项,选中在 client 分支里但不在 server 分支里的修改(即 C8 和 C9),将它们在 master 分支上重演:

    $ git rebase --onto master server client
    

    以上命令的意思是:“取出 client 分支,找出处于 client 分支和 server 分支的共同祖先之后的修改,然 后把它们在 master 分支上重演一遍”。

  2. 接下来你决定将 server 分支中的修改也整合进来。使用 git rebase [basebranch] [topicbranch] 命令可以直接将特性分支(即本例中的 server)变基到目标分支(即master)上。这样做能省去你先切换到 server 分支,再对其执行变基命令的多个步骤。

    $ git rebase master server
    

变基的风险 & 用变基解决变基

  1. 如果你习惯使用 git pull ,同时又希望默认使用选项 --rebase,你可以执行这条语句 git config --global pull.rebase true 来更改 pull.rebase 的默认配置。

  2. 假如你在那些已经被推送至共用仓库的提交上执行变基命令,并因此丢弃了一些别人的开发所基于的提交,那就有大麻烦了,你的同事也会因此鄙视你。

十、分布式工作流程

  • 集成管理者工作流 Git 允许多个远程仓库存在,使得这样一种工作流成为可能:每个开发者拥有自己仓库的写权限和其他所有人仓库的读权限。

十一、向一个项目贡献

提交准则

  1. 首先,你不会想要把空白错误(根据 git help diff 的描述,结合下面给出的图片,空白错误是指行尾的空格、Tab 制表符,和行首空格后跟 Tab 制表符的行为)提交上去。
    Git 提供了一个简单的方式来检查这点 - 在提交前,运行 git diff --check,它将会找到可能的空白错误并将它们为你列出来。

  2. 要知道必须合并什么进入,工作才能推送

    git log --no-merges issue54..origin/master
    

    issue54..origin/master 语法是一个日志过滤器,要求 Git 只显示所有在后面分支(在本例中是 origin/master)但不在前面分支(在本例中是 issue54)的提交的列表。

私有管理团队

  • 需要将在 featureB 分支上合并的工作推送到服务器上的 featureBee 分支。她可以通过指定 本地分支加上冒号(:)加上远程分支给 git push 命令来这样做:

    $ git push -u origin featureB:featureBee
    ...
    To jessica@githost:simplegit.git fba9af8..cd685d1 featureB -> featureBee
    

    注意 -u 标记;这是 --set-upstream 的简写,该标记会为之后轻松地推送与拉取配置分支

派生的公开项目

  • 因为你将分支变基了,所以必须为推送命令指定 -f 选项,这样才能将服务器上有一个不是它的后代的提交的 featureA 分支替换掉。一个替代的选项是推送这个新工作到服务器上的一个不同分支(可能称作 featureAv2)。

十二、选择修订版本

引用日志

  1. 当你在工作时, Git 会在后台保存一个引用日志(reflog),引用日志记录了最近几个月你的 HEAD 和分支引用所 指向的历史。 你可以使用 git reflog 来查看引用日志。

  2. 如果你想查看仓库中 HEAD 在五次前的所指向的提交,你可以使用 @{n} 来引用 reflog 中输出的提交记录。

    $ git show HEAD@{5}
    

    可以运行 git log -g 来查看类似于 git log 输出格式的引用日志信息

  3. 引用日志只存在于本地仓库,一个记录你在你自己的仓库里做过什么的日志。其他人拷贝的仓库里的引用日志不会和你的相同;而你新克隆一个仓库的时候,引用日志是空的,因为你在仓库里还没有操 作。
    git show HEAD@{2.months.ago} 这条命令只有在你克隆了一个项目至少两个月时才会有用——如果你是五分钟前克隆的仓库,那么它将不会有结果返回。

祖先引用

  1. 祖先引用是另一种指明一个提交的方式。如果你在引用的尾部加上一个 ^, Git 会将其解析为该引用的上一个提交。可以使用 git show HEAD^ 来查看上一个提交,也就是 “HEAD 的父提交”

  2. 可以在 ^ 后面添加一个数字——例如 d921970^2 代表 “d921970 的第二父提交”这个语法只适用于合并 (merge)的提交,因为合并提交会有多个父提交。第一父提交是你合并时所在分支,而第二父提交是你所合并的分支

  3. 另一种指明祖先提交的方法是 ~。同样是指向第一父提交,因此 HEAD~HEAD^ 是等价的。而区别在于你在后面加数字的时候。HEAD~2 代表 “第一父提交的第一父提交” —— Git 会根据你指定的次数获取对应的第一父提交。

  4. HEAD~3 也可以写成 HEAD^^^,也是第一父提交的第一父提交的第一父提交。也可以组合使用这两个语法 —— 你可以通过 HEAD~3^2 来取得之前引用的第二父提交(假设它是一个合并提交)

提交区间

  1. image

    想要查看 experiment 分支中还有哪些提交尚未被合并入 master 分支。你可以使用 master..experiment 来让 Git 显示这些提交。也就是 “在 experiment 分支中而不在 master 分支中的提交”。
    这可以让你保持 experiment 分支跟随最新的进度以及查看你即将合并的内容。

  2. 另一个常用的场景是查看你即将推送到远端的内容:

    $ git log origin/master..HEAD
    

    这个命令会输出在你当前分支中而不在远程 origin 中的提交。
    如果你留空了其中的一边, Git 会默认为 HEAD。例如, git log origin/master.. 将会输出与之前例子相同的结果 —— Git 使用 HEAD 来代替留空的一边

多点

  1. Git 允许你在任意引用前加上 ^ 字符或者 --not 来指明你不希望提交被包含其中的分支。因此下列3个命令是等价的:

    $ git log refA..refB 
    $ git log ^refA refB 
    $ git log refB --not refA
    
  2. 这个语法可以帮你在查询中指定超过两个的引用,例如想查看所有被 refA 或 refB 包含的但是不被 refC 包含的提交,你可以输入下面中的任意一个命令

    $ git log refA refB ^refC
    $ git log refA refB --not refC
    

三点

  • 最后一种主要的区间选择语法是三点,这个语法可以选择出被两个引用中的一个包含但又不被两者同时包含的提交。

    $ git log master...experiment
    F
    E
    D
    C
    

    可以使用参数 --left-right,它会显示每个提交到底处于哪一侧的分支。

    $ git log --left-right master...experiment 
    < F
    < E
    > D
    > C
    

十三、交互式暂存

运行 git add 时使用 -i 或者 --interactive 选项,Git 将会进入一个交互式终端模式

十四、储藏与清理

储藏工作

  1. 将新的储藏推送到栈上,运行 git stashgit stash save

  2. 要查看储藏的东西,可以使用 git stash list

    $ git stash list
    stash@{0}: WIP on master: 049d078 added the index file 
    stash@{1}: WIP on master: c264051 Revert "added file_size"
    stash@{2}: WIP on master: 21d80a5 added number to log
    
  3. 将你刚刚储藏的工作重新应用:git stash apply 。如果想要应用其中一个更旧的储藏,可以通过名字指定它,像这样:git stash apply stash@{2}。如果不指定一个储藏,Git 认为指定的是最近的储藏.

  4. 可以在一个分支上保存一个储藏,切换到另一个分支,然后尝试重新应用这些修改。当应用储藏时工作目录中也可以有修改与未提交的文件 - 如果有任何东西不能干净地应用,Git 会产生合并冲突。

  5. 文件的改动被重新应用了,但是之前暂存的文件却没有重新暂存。想要那样的话,必须使用 --index 选项来运行 git stash apply 命令,来尝试重新应用暂存的修改(即存放到相应的暂存区)。

  6. 应用选项只会尝试应用暂存的工作 - 在堆栈上还有它。可以运行 git stash drop 加上将要移除的储藏的名字来移除它。
    也可以运行 git stash pop 来应用储藏然后立即从栈上扔掉它。

创造性的储藏

  1. 有几个储藏的变种可能也很有用。第一个非常流行的选项是 stash save 命令的 --keep-index 选项。它告诉 Git 不要储藏任何你通过 git add 命令已暂存的东西。
    当你做了几个改动并只想提交其中的一部分,过一会儿再回来处理剩余改动时,这个功能会很有用。

  2. 另一个是像储藏跟踪文件一样储藏未跟踪文件。默认情况下,git stash 只会储藏已经在索引中的文件。如果指定 --include-untracked 或 -u 标记,Git 也会储藏任何创建的未跟踪文件。

从储藏创建一个分支

  • 如果储藏了一些工作,将它留在那儿了一会儿,然后继续在储藏的分支上工作,在重新应用工作时可能会有问题。
    如果应用尝试修改刚刚修改的文件,你会得到一个合并冲突并不得不解决它。
    如果想要一个轻松的方式来再次测试储藏的改动,可以运行 git stash branch 创建一个新分支,检出储藏工作时所在的提交,重新在那应用工作,然后在应用成功后扔掉储藏

    $ git stash branch testchanges 
    Switched to a new branch "testchanges" # On branch testchanges # Changes to be committed:
    
    # (use "git reset HEAD <file>..." to unstage) 
    # 
    # modified: index.html
    # 
    # Changed but not updated:
    # (use "git add <file>..." to update what will be committed)
    # 
    # modified: lib/simplegit.rb 
    # Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359)
    

    这是在新分支轻松恢复储藏工作并继续工作的一个很不错的途径。

十五、搜索

Git Grep

  1. Git 提供了一个 grep 命令,你可以很方便地从提交历史或者工作目录中查找一个字符串或者正则表达式。
    相比于一些常用的搜索命令比如 grepackgit grep 命令有一些的优点。第一就是速度非常快,第二是你不仅仅可以搜索工作目录,还可以搜索任意的 Git 树。

  2. 默认情况下 Git 会查找你工作目录的文件。你可以传入 -n 参数来输出 Git 所找到的匹配行行号

    $ git grep -n gmtime_r 
    compat/gmtime.c:3:#undef gmtime_r 
    compat/gmtime.c:8: return git_gmtime_r(timep, &result); 
    compat/mingw.c:606:struct tm *gmtime_r(const time_t *timep, struct tm *result)
    date.c:429: if (gmtime_r(&now, &now_tm))
    
  3. 你可以使用 --count 选项来使 Git 输出概述的信息,仅仅包括哪些文件包含匹配以及每个文件包含了多少个匹配。

    $ git grep --count gmtime_r 
    compat/gmtime.c:2
    compat/mingw.c:1  
    date.c:1
    
  4. 如果你想看匹配的行是属于哪一个方法或者函数,你可以传入 -p 选项:$ git grep -p gmtime_r *.c

  5. 你还可以使用 --and 标志来查看复杂的字符串组合,也就是在同一行同时包含多个匹配。

  6. 如果我们想找到 ZLIB_BUF_MAX 常量是什么时候引入的,我们可以使用 -S 选项来显示新增和删除该字符串的提交。

    $ 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 addgit rm 一个已追踪的文件,随后运行 git commit --amend 拿走当前的暂存区域并使其做为新提交的快照。
    使用这个技巧的时候需要小心,因为修正会改变提交的 SHA-1 校验和。它类似于一个小的变基 - 如果已经推送了最后一次提交,就不要修正它。

修改多个提交信息

  • 例如,如果想要修改最近三次提交信息,或者那组提交中的任意一个提交信息,将想要修改的最近一次提交的父提交作为参数传递给 git rebase -i命令,即 HEAD~2^HEAD~3。记住 ~3 可能比较容易,因为你正尝试修改最后三次提交;
    但是注意实际上指定了以前的四次提交,即想要修改提交的父提交: $ git rebase -i HEAD~3 再次记住这是一个变基命令 - 在 HEAD~3..HEAD 范围内的每一个提交都会被重写,无论你是否修改信息。

重新排序提交

压缩提交

拆分提交

核武器级选项:filter-branch

从每一个提交移除一个文件
  • filter-branch 是一个可能会用来擦洗整个提交历史的工具。为了从整个提交历史中移除一个叫做 passwords.txt 的文件,可以使用 --tree-filter 选项给 filter-branch

    $ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
    Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21) 
    Ref 'refs/heads/master' was rewritten
    
全局修改邮箱地址

十七、重置揭密

三棵树 HEAD, Index, Working Directory

  1. HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。这表示 HEAD 将是下一次提交的父结点。通常,理解 HEAD 的最简方式,就是将它看做 你的上一次提交 的快照。

  2. 索引是你的 预期的下一次提交。我们也会将这个概念引用为 Git 的 “暂存区域”,这就是当你运行 git commit 时 Git 看起来的样子。

重置的作用

  • 如果指定 --mixed 选项,会撤销一上次 提交,但还会 取消暂存 所有的东西。于是,我们回滚到了所有 git add 和 git commit 的命令执行之前,所有修改都存在于 Working Directory

通过路径来重置

  • 你还可以给 reset 提供一个作用路径。若指定了一个路径,reset 会将它的作用范围限定为指定的文件或文件集合。这样做自然有它的道理,因为 HEAD 只是一个指针,你无法让它同时指向两个提交中各自的一部分。不过索引和工作目录可以部分更新,所以重置会继续进行第 2、3 步。
    假如我们运行 git reset file.txt(这其实是 git reset --mixed HEAD file.txt 的简写形 式,因为你既没有指定一个提交的 SHA-1 或分支,也没有指定 --soft--hard),它会:
    1. 移动 HEAD 分支的指向 (已跳过)
    2. 让索引看起来像 HEAD (到此处停止)
      所以它本质上只是将 file.txt 从 HEAD 复制到索引中。

压缩

检出

  • 假设我们有 master 和 develop 分支,它们分别指向不同的提交;我们现在在 develop 上(所以 HEAD 指向它)。如果我们运行 git reset master,那么 develop 自身现在会和 master 指向同一个提交。而如果我们运行 git checkout master 的话,develop 不会移动,HEAD 自身会移动。现在 HEAD 将 会指向 master。
    image

总结

  • 下面的速查表列出了命令对树的影响。“HEAD” 一列中的 “REF” 表示该命令移动了 HEAD 指向的分支引 用,而`‘HEAD’' 则表示只移动了 HEAD 自身。特别注意 WD Safe? 一列 - 如果它标记为 NO,那么运行该命令 之前请考虑一下。


    image

十八、高级合并

合并冲突

  1. git merge --abort 选项会尝试恢复到你运行合并前的状态。但当运行命令前,在工作目录中有未储藏、未提交的修改时它不能完美处理,除此之外它都工作地很好。

  2. 一个很有用的工具是带 --conflict 选项的 git checkout。这会重新检出文件并替换合并冲突标记。如果想要重置标记并尝试再次解决它们的话这会很有用。 可以传递给--conflict 参数 diff3merge(默认选项)。如果传给它 diff3,Git 会使用一个略微不同版本的冲突标记:不仅仅只给你 “ours” 和 “theirs” 版本,同时也会有 “base” 版本在中间来给你更多的上下文。

    $ git checkout --conflict=diff3 hello.rb
    
    #! /usr/bin/env ruby
    
    def hello 
    <<<<<<< ours
      puts 'hola world' 
    ||||||| base
      puts 'hello world'
    =======
      puts 'hello mundo'
    >>>>>>> theirs 
    end
    
    hello()
    

十九、使用 Git 调试

文件标注

  1. 你可以使用 git blame 标注这个文件,查看这个方法每一行的最后修改时间以及是被谁修改的,可以使用 -L 选项来限制输出范围

  2. 有一个很有意思的特性就是你可以让 Git 找出所有的代码移动。如果你在 git blame 后面加上一个 -C,Git 会分析你正在标注的文件,并且尝试找出文件中从别的地方复制过来的代码片段的原始出处。
    比如,你将 GITServerHandler.m 这个文件拆分为数个文件,其中一个文件是 GITPackUpload.m。对 GITPackUpload.m 执行带 -C 参数的blame命令,你就可以看到代码块的原始出处

    $ git blame -C -L 141,153 GITPackUpload.m
    

二分查找

  1. 首先执行 git bisect start 来启动,接着执行 git bisect bad 来告诉系统当前你所在的提交是有问题的。然后你必须告诉 bisect 已知的最后一次正常状态是哪次提交,使用 git bisect good [good_commit]

    $ git bisect start 
    $ git bisect bad
    $ git bisect good v1.0 
    Bisecting: 6 revisions left to test after this 
    [ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo
    

    假设测试结果是没有问题的,你可以通过 git bisect good 来告诉 Git,然后继续寻找。
    你再一次执行测试,发现这个提交下是有问题的,因此你可以通过 git bisect bad 告诉 Git

  2. 当你完成这些操作之后,你应该执行 git bisect reset 重置你的 HEAD 指针到最开始的位置,否则你会停留在一个很奇怪的状态

二十、配置 Git

外部的合并与比较工具

格式化与多余的空白字符

二十一、Git 属性

合并策略

  • 通过 Git 属性,你还能对项目中的特定文件指定不同的合并策略。一个非常有用的选项就是,告诉 Git 当特定文件发生冲突时不要尝试合并它们,而是直接使用你这边的内容。

二十二、Git 钩子

客户端钩子

  1. pre-commit 钩子在键入提交信息前运行。它用于检查即将提交的快照,例如,检查是否有所遗漏,确保测试运行,以及核查代码。如果该钩子以非零值退出,Git 将放弃此次提交,不过你可以用 git commit --no -verify 来绕过这个环节。你可以利用该钩子,来检查代码风格是否一致(运行类似 lint 的程序)、尾随空白字符是否存在(自带的钩子就是这么做的),或新方法的文档是否适当。

  2. post-commit 钩子在整个提交过程完成后运行。它不接收任何参数,但你可以很容易地通过运行 git log -1 HEAD 来获得最后一次的提交信息。该钩子一般用于通知之类的事情。

二十三、Git 对象

  1. 可以通过底层命令 hash-object 来演示上述效果——该命令可 将任意数据保存于 .git 目录,并返回相应的键值。

  2. -w 选项指示 hash-object 命令存储数据对象;若不指定此选项,则该命令仅返回对应的键值。--stdin 选项则指示该命令从标准输入读取内容;若不指定此选项,则须在命令尾部给出待存储文件的路径。

  3. 可以通过 cat-file 命令从 Git 那里取回数据。这个命令简直就是一把剖析 Git 对象的瑞士军刀。为 cat-file 指定 -p 选项可指示该命令自动判断内容的类型,并为我们显示格式友好的内容

  4. 利用 cat-file -t 命令,可以让 Git 告诉我们其内部存储的任何对象类型,只要给定该对象的 SHA-1 值

树对象

  1. 所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。

  2. master^{tree} 语法表示 master 分支上最新的提交所指向的树对象。

  3. Git 根据某一时刻暂存区(即 index 区域,下同)所表示的状态创建并记录一个对应的树对象,如此重复便可依次记录(某个时间段内)一系列的树对象。
    可以通过底层命令 update-index 为一个单独文件创建一个暂存区。
    必须为上述命令指定 --add 选项,因为此前该文件并不在暂存区中
    同样必需的还有 --cacheinfo 选项,因为将要添加的文件位于 Git 数据库中,而不是位于当前目录下。

  4. 文件模式有 100644,表明这是一个普通文件;100755,表示一个可执行文件;120000,表示一个符号链接。

  5. 可以通过 write-tree 命令将暂存区内容写入一个树对象。此处无需指定 -w 选项——如果某个树对象此 前并不存在的话,当调用 write-tree 命令时,它会根据当前暂存区状态自动创建一个新的树对象

  6. 通过调用 read-tree 命令,可以把树对象读入暂存区。本例中,可以通过对 read-tree 指定 --prefix 选项,将一个已有的树对象作为子树读入暂存区

  7. 如果基于这个新的树对象创建一个工作目录,你会发现工作目录的根目录包含两个文件以及一个名为 bak 的子目录,该子目录包含 test.txt 文件的第一个版本

提交对象

  1. 这三个提交对象分别指向之前创建的三个树对象快照中的一个。现在,如果对最后一个提交的 SHA-1 值运行 git log 命令,会出乎意料的发现,你已有一个货真价实的、可由 git log 查看的 Git 提交历史了

  2. 这就是每次我们运行 git add 和 git commit 命令时, Git 所做的实质工作——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。
    这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 .git/objects 目录下。

二十四、Git 引用

  1. 这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。

  2. 当运行类似于 git branch (branchname) 这样的命令时,Git 实际上会运行 update-ref 命令,取得当前 所在分支最新提交对应的 SHA-1 值,并将其加入你想要创建的任何新引用中。

HEAD 引用

  1. HEAD 文件是一个符号引用(symbolic reference),指向目前所在的分支。所谓符号引用,意味着它并不像普通引用那样包含一个 SHA-1 值——它是一个指向其他引用的指针。

  2. 当我们执行 git commit 时,该命令会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置其父提交字段。

二十五、维护与数据恢复

数据恢复

  1. 最方便最常用的方法,是使用一个名叫 git reflog 的工具。当你正在工作时,Git 会默默地记录每一次你改变 HEAD 时它的值。每一次你提交或改变分支,引用日志都会被更新。引用日志(reflog)也可以通过 git update-ref 命令更新。

  2. 由于引用日志数据存放在 .git/logs/ 目录中,现在你已经没有引用日志了。这时该如何恢复那次提交?一种方式是使用 git fsck 实用工具,将会检查数据库的完整性。如果使用一个 --full 选项运行它,它会向你显示出所有没有被其他对象指向的对象

移除对象

  1. 警告:移除对象的操作对提交历史的修改是破坏性的。它会从你必须修改或移除一个大文件引用最早的树对象开始重写 每一次提交。如果你在导入仓库后,在任何人开始基于这些提交工作前执行这个操作,那么将不会有任何问题 否则,你必须通知所有的贡献者他们需要将他们的成果变基到你的新提交上。

  2. 执行 gc 来查看数据库占用了多少空间,也可以执行 count-objects 命令来快速的查看占用空间大小。
    假设你不知道该如何找出哪个文件或哪些文件占用了如此多的空间。如果你执行 git gc 命令,所有的对象将被放入一个包文件中。
    你可以通过运行 git verify-pack 命令,然后对输出内容的第三列(即文件大小)进行排序,从而找出这个大文件。
    你也可以将这个命令的执行结果通过管道传送给 tail 命令,因为你只需要找到列在最后的几个大对象。
    为了找出具体是哪个文件,可以使用 revlist 命令,我们在 指定特殊的提交信息格式 中曾提到过。如果你传递 --objects 参数给 rev-list 命令, 它就会列出所有提交的 SHA-1、数据对象的 SHA-1 和与它们相关联的文件路径。可以使用以下命令来找出你的数据对象的名字

  3. 你必须重写 7b30847 提交之后的所有提交来从 Git 历史中完全移除这个文件。为了执行这个操作,我们 要使用 filter-branch 命令。

    $ git filter-branch --index-filter \ 
    'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
    
    Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz' 
    Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2) 
    Ref 'refs/heads/master' was rewritten
    

    --index-filter 选项类似于在 重写历史 中提到的的 --tree-filter 选项,不过这个选项并不会让命令将修改在硬盘上检出的文件,而只是修改在暂存区或索引中的文件。
    你必须使用 git rm --cached 命令来移除文件,而不是通过类似 rm file 的命令 - 因为你需要从索引中移除它,而不是磁盘中。
    还有一个原因是速度 - Git 在运行过滤器时,并不会检出每个修订版本到磁盘中,所以这个过程会非常快。如果愿意的话,你也可以通过 --tree-filter 选项来完成同样的任务。
    git rm 命令的 --ignore-unmatch 选项告诉命令:如果尝试删除的模式不存在时,不提示错误。
    最后,使用 filter-branch 选项来重写自 7b30847 提交以来的历史,也就是这个问题产生的地方。否则,这个命令会从最旧的提交开始,这将会花费许多不必要的时间。

  4. 你的历史中将不再包含对那个文件的引用。不过,你的引用日志和你在 .git/refs/original 通过 filter-branch 选项添加的新引用中还存有对这个文件的引用,所以你必须移除它们然后重新打包数据库。在重新打包前需要移除任何包含指向那些旧提交的指针的文件……

推荐阅读更多精彩内容

  • 取得项目的Git仓库 初始化 添加文件 克隆 记录每次更新到仓库 查看文件状态 跟踪新文件 忽略文件 # 此为注释...
    Sacowiw阅读 93评论 0 1
  • 这篇文章主要站在用户的角度,在实际使用过程中常见git的命令 git remote - 管理远程仓库 示例 git...
    gsonliu阅读 139评论 0 1
  • Git学习笔记 常用命令 查看提交历史 git log 一个常用选项是 -p,用来显示每次提交的内容差异。 -n显...
    悲伤的盖茨比阅读 165评论 0 0
  • sha-1 git大量使用sha-1进行数据的标识,关于sha-1可以参考md5与sha-1 内容寻址系统 sha...
    allbugkiller阅读 145评论 0 0
  • git 帮助 git的三种状态 已提交(committed),已修改(modified)和已暂存(staged) ...
    RevinDuan阅读 601评论 0 0