了解和熟悉下面的Git工具,会使你毫无压力地在命令行中使用Git来完成日常中的大部分事情。
六、Git工具
1. 选择修订版本
Git允许通过几种方法来指明特定的或者一定范围内的提交。
git show <commitid>
git show <简短的SHA-1>
SHA-1 的前几个字符就可以获得对应的那次提交,当然你提供的 SHA-1 字符数量不得少于4个,并且没有歧义——也就是说,当前仓库中只有一个对象以这段 SHA-1 开头。
# --abbrev-commit显示简短且唯一的值
$ git log --abbrev-commit --pretty=oneline
(1)引用日志
Git会在后台保存一个引用日志(reflog),引用日志记录了最近几个月你的 HEAD 和分支引用所指向的历史。
$ git reflog
每当HEAD所指向的位置发生了变化,Git就会将这个信息存储到引用日志这个历史记录里。
# 显示制定提交记录
$ git show HEAD@{21}
# 显示昨天提交记录
$ git show [email protected]{yesterday}
(2)祖先引用
# 查看上一个提交
$ git show HEAD^
# 查看d921970的祖父提交
$ git show d921970~2
(3)提交区间
# 在develop分支中而不在master分支中的提交
$ git log master..develop
# 在master分支中而不在develop分支中的提交
$ git log develop..master
# 在你当前分支中而不在远程 origin 中的提交
$ git log origin/master..HEAD
# 查看所有被refA或refB包含的但是不被refC包含的提交
$ git log refA refB ^refC
$ git log refA refB --not refC
# 选择出被两个引用中的一个包含但又不被两者同时包含的提交
$ git log --left-right master...develop
2. 交互式暂存
当你修改一组文件后,希望这些改动能放到若干提交而不是混杂在一起成为一个提交时,交互式暂存变得非常有用。
$ git add -i/--interactive
3. 储藏与清理
当你在项目的一部分上已经工作一段时间后,所有东西都进入了混乱的状态,而这时你想要切换到另一个分支做一点别的事情。问题是,你不想仅仅因为过会儿回到这一点而为做了一半的工作创建一次提交。针对这个问题的答案是git stash命令。其会将修改的文件保存到一个栈上,而你可以在任何时候重新应用这些改动。
$ git stash
$ git stash save
# 查看储藏的东西
$ git stash list
# 重新应用储藏
$ git stash apply [email protected]{2}
注意:
- 可以在一个分支上保存一个储藏,切换到另一个分支,然后尝试重新应用这些修改
- 当应用储藏时工作目录中也可以有修改与未提交的文件,如果有任何东西不能干净地应用,Git会产生合并冲突。
# 从栈上删除储藏
$ git stash drop [email protected]{2}
# 应用后立即删除
$ git stash pop
(1)创造性的储藏
不储藏任何你通过 git add 命令已暂存的东西
$ git stash --keep-index
git stash
只会储藏已经在索引中的文件。
如果指定 --include-untracked
或 -u
标记,Git也会储藏任何创建的未跟踪文件。
$ git stash -u
(2)从储藏创建一个分支
$ git stash branch <branchname> <stash>
其创建一个新分支,检出储藏工作时所在的提交,重新在那应用工作,然后在应用成功后扔掉储藏。是在新分支轻松恢复储藏工作并继续工作的一个很不错的途径。
(3)清理工作目录
移除工作目录中所有未追踪的文件以及空的子目录(-f意味着“强制”或“确定移除”)。
$ git clean -f -d
-n 选项来运行命令,这意味着 “做一次演习然后告诉你 将要 移除什么”。
$ git clean -d -n
更安全的方式,将所有东西放到储藏栈中,同样达到了清理工作目录的目的。
git stash --all
4. 签署工作
每个人生成私钥,用生成的密钥来签署标签与提交。
5. 搜索
(1)浏览代码
grep命令,可以很方便地从提交历史或者工作目录中查找一个字符串或者正则表达式。
# 搜索所有文件中包含“ligang”的文件,并显示行号
$ git grep -n ligang
# 输出概要信息
$ git grep --count ligang
# 想看匹配的行是属于哪一个方法或者函数
$ git grep -p js-pt-settings-user
$ git grep --break --heading -p js-pt-settings-user
- –break:多个文件之间空行隔开
- –heading:文件名独占一行
(2)日志搜索
# 想知道是什么时候存在或者引入的静态常量"ZLIB_BUF_MAX"
$ git log -SZLIB_BUF_MAX --oneline
$ git log -Sligang --oneline
# 行日志搜索
# 查看zlib.c文件中git_deflate_bound函数的每一次变更
$ git log -L :git_deflate_bound:zlib.c
5. 重写历史
(1)修改最后一次提交
对最近一次提交,修改提交信息,或者修改你添加、修改和移除的文件的快照。
$ git commit --amend
注意:其修正会改变提交的SHA-1校验和,类似于一个小的变基。如果已经推送了最后一次提交就不要修正它。
(2)修改多个提交信息
为了修改在提交历史中较远的提交,必须使用更复杂的工具。Git没有一个改变历史工具,但是可以使用变基工具来变基一系列提交。
# 在HEAD~3..HEAD范围内的每一个提交都会被重写,无论你是否修改信息
$ git rebase -i HEAD~3
6. 重置揭密
(1)三棵树
理解reset和checkout的最简方法,就是以Git的思维框架(将其作为内容管理器)来管理三棵不同的树。“树” 在我们这里的实际意思是“文件的集合”,而不是指特定的数据结构。
Git 作为一个系统,是以它的一般操作来管理并操纵这三棵树的:
树 | 用途 |
---|---|
HEAD | 上一次提交的快照,下一次提交的父结点 |
Index | 预期的下一次提交的快照 |
Working Directory | 沙盒 |
HEAD:HEAD是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做你的上一次提交的快照。
# 显示了 HEAD 快照实际的目录列表
$ git cat-file -p HEAD
Index:索引是你的“预期的下一次提交”–“暂存区域”,运行git add后,代码就进入“暂存区域”。
# 显示出索引当前的样子
$ git ls-files -s
Working Directory:可以把工作目录当做“沙盒”。在将修改提交到暂存区并记录到历史之前,可以随意更改。
(2)工作流程
Git主要的目的是通过操纵这三棵树来以更加连续的状态记录项目的快照。
(3)重置的作用
将当前的分支重设(reset)到指定的<commit>
或者HEAD
(默认是HEAD,即最新的一次提交),并且根据[mode]有可能更新index和working directory(默认是mixed)。
$ git reset [--hard|soft|mixed|merge|keep] [commit|HEAD]
A). –hard:重设index和working directory,从<commit>
以来在working directory中的任何改变都被丢弃,并把HEAD指向<commit>
。
$ git add test1
$ git commit -m"Add test1"
$ git add test2
$ git commit -m"Add test2"
$ git log --oneline
$ git reset --hard HEAD~1
$ git log --oneline
$ git status #没有任何内容
说明:第二次提交的test2已被丢弃!HEAD指针重新指向了第一次提交的commitID。彻底回退到某个版本,本地的源码也会变为上一个版本的内容。
B). –soft:index和working directory中的内容不作任何改变,仅仅把HEAD指向<commit>
。自从<commit>
以来的所有改变都会显示在git status的“Changes to be committed”中。
$ git add test1
$ git commit -m"Add test1"
$ git add test2
$ git commit -m"Add test2"
$ git log --oneline
$ git reset --soft HEAD~1 #不会有任何提示
$ git log --oneline
$ git status
说明:第二次提交的test2被重置到了”Changes to be committed”中!HEAD指针重新指向了第一次提交的commitID。回退到某个版本,只回退了commit的信息。如果还要提交,直接commit即可。
C). –mixed:仅重设index,但是不重设working directory。这个模式是默认模式,即当不显示告知git reset
模式时,会使用mixed模式。这个模式的效果是,working directory中文件的修改都会被保留,不会丢弃,但是也不会被标记成“Changes to be committed”,但是会打出什么还未被更新的报告。
$ git add test1
$ git commit -m"Add test1"
$ git add test2
$ git commit -m"Add test2"
$ git log --oneline
#不会有任何提示(默认方式,可以省略--mixed)#不会有任何提示(默认方式,可以省略--mixed)
$ git reset --mixed HEAD~1
$ git log --oneline
$ git status
说明:第二次提交的test2被重置到了初始状态(上述示例为“Untracked”)!HEAD指针重新指向了第一次提交的commitID。回退到某个版本,只保留源码,回退commit和index信息。
(4)reset常用示例
A). 回退add操作
$ git add test
$ git reset HEAD test
说明:可以将test从“已暂存”状态(Index区)回滚到初始状态。
B). 回退最后一次提交
$ git add test
$ git commit -m"Add test"
$ git reset --soft HEAD^
说明:可以将test从“已提交”状态变为“已暂存”状态。
C). 回退最近几次提交,并把这几次提交放到新分支上
$ git branch topic #已当前分支为基础,新建分支topic
$ git reset --hard HEAD~2 #在当前分支上回滚提交
$ git checkout topic
说明:通过临时分支来保留提交,然后在当前分支上做硬回滚。
D). 将本地的状态回退到和远程一样
$ git reset --hard origin/devlop
E). 回退到某个版本提交
$ git reset 497e350
说明:497e350为某commitID。当前HEAD会指向497e350,在497e350其之后提交的内容会被回退到初始状态。
F). 要想在develop分支,但错误地提交到了maser分支
git checkout master
git add .
git commit -m"..."
git reset --mixed HEAD~1
git stash
git checkout develop
git stash pop
8. 高级合并
合并和提交并无不同!!!
在Git中合并是相当容易的,不像其他的版本控制系统,Git 并不会尝试过于聪明的合并冲突解决方案。Git的哲学是聪明地决定无歧义的合并方案,但是如果有冲突,它不会尝试智能地自动解决它。
(1)合并冲突
首先,在做一次可能有冲突的合并前尽可能保证工作目录是干净的。如果你有正在做的工作,要么提交到一个临时分支要么储藏它。这使你可以撤消在这里尝试做的任何事情。
# 忽略任意”数量“的已有空白的修改
$ git merge -Xignore-all-space whitespace
# 忽略所有空白修改
$ git merge -Xignore-space-change whitespace
(2)手动合并
当在不同分支或同一分支不同开发者同时修改了同一文件,会产生冲突。这时,我们只想保留某人的修改。
示例:
A同学在develop分支上对test.js进行了修改
$ git checkout develop
$ vi test.js
$ git add test.js
$ git commit -m"Mod test"
$ git push origin develop
B同学在maser分支上对test.js进行了修改
$ git checkout master
$ vi test.js
$ git add test.js
$ git commit -m"Mod test"
$ git push origin master
C同学此时切换到master分支,但只想保留A同学的修改
$ git checkout master
$ git pull
$ git merge --no-ff develop
#由于对同一文件进行了修改,此时会产生冲突
$ git checkout --theirs test.js #只保留A同学的修改
$ git diff --ours #查看和B同学修改的差别
$ git checkout --ours test.js #只保留B同学的修改
$ git diff --theirs #查看和A同学修改的差别
注意:在把握不好哪个是ours的时候,有个简单的方法就是打开那个文件,HEAD代表ours。
(3)撤销合并
假设现在在一个特性分支上工作,不小心将其合并到master中。
方式一:修复引用
如果这个不想要的合并提交只存在于你的本地仓库中,最简单且最好的解决方案是移动分支到你想要它指向的地方。
# 移动到合并前的提交点
$ git reset --hard HEAD~1
这个方法的缺点是它会重写历史,在一个共享的仓库中这会造成问题的。如果其他人已经有你将要重写的提交,你应当避免使用 reset。 如果有任何其他提交在合并之后创建了,那么这个方法也会无效;移动引用实际上会丢失那些改动。
方式二:还原提交
撤销上次提交的所有修改
$ git revert -m 1 HEAD
-m 1 标记指出 “mainline” 需要被保留下来的父结点。上述示例为以上次提交的结点为当前主线父节点。同理:$ git revert -m 1 HEAD~3
表示最近3次的提交会被干掉。
新的提交 ^M 与 C6 有完全一样的内容,所以从这儿开始就像合并从未发生过,除了“现在还没合并”的提交依然在 HEAD 的历史中。
(4)快速合并
默认情况下,当 Git 看到两个分支合并中的冲突时,它会将合并冲突标记添加到你的代码中并标记文件为冲突状态来让你解决。 如果你希望 Git 简单地选择特定的一边并忽略另外一边而不是让你手动合并冲突,你可以传递给 merge 命令一个 -Xours 或 -Xtheirs 参数。
$ git merge -Xours develop
9. Rerere
rerere(“reuse recorded resolution”)它允许你让Git记住解决一个块冲突的方法,这样在下一次看到相同冲突时,Git可以为你自动地解决它。
(1)启用rerere
方式一:运行配置项,开启
$ git config --global rerere.enabled true
方式二:也通过在特定的仓库中创建 .git/rr-cache
目录来开启它
注意:设置选项更干净并且可以应用到全局,推荐使用配置项开启
(2)示例
步骤一:制造冲突
在develop和master分支上,同时对test.js文件中的同一行进行修改
develop分支修改为:console.log(“develop”)
master分支修改为:console.log(“master”)
步骤二:产生冲突,并手动解决
切换到master分支,合并develop分支的内容
$ git checkout master
$ git merge --no-ff develop
发现其比正常合并冲突会多一行“Recorded preimage for FILE”的提示。
$ git rerere status
$ git rerere diff
显示解决方案的当前状态、开始解决前与解决后的样子
$ git ls-files -u
显示冲突文件的之前、左边与右边版本。可以通过下述命令,检索出对应版本文件:
$ git show :1:test.js > test.common.js
$ git show :2:test.js > test.ours.js
$ git show :3:test.js > tset.theirs.js
步骤三:提交解决的冲突
我们将两个console同时留下,并提交
$ git add .
$ git commit -m"Mod conflict"
$ git push origin master
步骤四:再次制造相同冲突(重复步骤一)
步骤五:产生冲突
$ git checkout master
$ git merge --no-ff develop
发现其会按照上次的解决方案,自动解决冲突。
无需手动解决冲突,方可直接add、commit。
(3)恢复文件到冲突状态
rerere可以帮我们按之前的解决方案,解决历史出现的冲突。如果,我们不想按历史的方案解决,该如何处理呢?
$ git checkout --conflict=merge test.js
10. 使用Git调试
Git提供了两个工具辅助我们在项目中出现问题的时候帮助我们找到bug或者错误。
(1)文件标注
如果你在追踪代码中的一个bug,并且想知道是什么时候以及为何会引入,文件标注通常是最好用的工具。
它展示了文件中每一行最后一次修改的提交。
$ git blame -L 75,80 server/app.js
其中,-L选项来限制输出范围
另一件比较酷的事情是Git不会显式地记录文件的重命名。它会记录快照,然后在事后尝试计算出重命名的动作。这其中有一个很有意思的特性就是你可以让Git找出所有的代码移动。如果你在 git blame
后面加上一个 -C,Git会分析你正在标注的文件,并且尝试找出文件中从别的地方复制过来的代码片段的原始出处。
$ git blame -C -L 75,80 server/app.js
(2)二分查找
假设你刚刚在线上环境部署了你的代码,接着收到一些bug反馈,但这些bug在你之前的开发环境里没有出现过,这让你百思不得其解。 你重新查看了你的代码,发现这个问题是可以被重现的,但是你不知道哪里出了问题。你可以用二分法来找到这个问题。
步骤一:启动二分查找,并告知Git当前所在的提交是有问题的
$ git bisect start
$ git bisect bad
步骤二:告诉bisect已知的最后一次正常状态是哪次提交
$ git bisect good [good_commit]
此命令会告知你,在你标记为正常的提交和当前的错误版本之间有大约提交的数。
步骤三:git自动检出Git检出中间的那个提交,然后需你测试验证是否有问题
- 如果还存在,说明问题是在这个提交之前引入的;
- 如果问题不存在,说明问题是在这个提交之后引入的。
# good标识测试ok
$ git bisect good
# bad标识测试error
$ git bisect bad
注意:在标识检索出来的提交是否有问题时,git会返回给我们类似“Bisecting: 3 revisions left to test after this”这样的提示。
步骤四:重复上述第三步骤,知道发现错误提交
步骤五:成功找出错误提交,重置你的HEAD指针
$ git bisect reset
注意:当你完成这些操作之后,必须重置HEAD,否则你会停留在一个很奇怪的状态。
感慨:通过每次手动测试检索出来的提交是否正确,是不现实的,工作量太大。但是通过自动化测试脚本会很爽的。如:
# 设定好项目正常以及不正常所在提交的二分查找范围
# 第一个参数(HEAD)是项目不正常的提交,第二个参数(good_commit)是项目正常的提交
$ git bisect start HEAD [good_commit]
$ git bisect run test-error.sh
Git会自动在每个被检出的提交里执行test-error.sh直到找到第一个项目不正常的提交。你也可以执行make或者make tests或者其他东西来进行自动化测试。
注意:你的测试脚本必须约定:在项目是正常的情况下返回0,在不正常的情况下返回非0
(3)总结
当你知道问题是在哪里引入的情况下文件标注可以帮助你查找问题;
如果你不知道哪里出了问题,并且自从上次可以正常运行到现在已经有数十个或者上百个提交,可以使用二分查找定位问题。
11. 子模块
经常会遇到:某个工作中的项目需要包含并使用另一个项目;想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。
Git通过子模块来解决这个问题。子模块允许你将一个Git仓库作为另一个Git仓库的子目录。它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
(1)添加新的子模块
# 在项目test中添加子模块t-module
$ git submodule add https://github.com/381510688/t-module.git
默认情况下,子模块会将子项目放到一个与仓库同名的目录中,本例中是 “t-module”。如果你想要放到其他地方,那么可以在命令结尾添加一个不同的路径。
.gitmodules
文件中保存了项目 URL 与已经拉取的本地目录之间的映射。如果有多个子模块,该文件中就会有多条记录。
(2)当你不在子项目目录中时,Git并不会跟踪它的内容,而是将它看作该仓库中的一个特殊提交
$ git diff --cached t-module
$ git diff --cached --submodule
(3)克隆含有子模块的项目
方式一:克隆项目,然后初始化更新子项目
$ git clone https://github.com/381510688/test.git
注意:其中有子模块t-moudle目录,不过是空的。
# 初始化本地配置文件
$ git submodule init
# 从该项目中抓取所有数据并检出父项目中列出的合适的提交
$ git submodule update
方式二:克隆项目,自动初始化并更新仓库中的每一个子模块
$ git clone --recursive https://github.com/381510688/test.git
(4)获取子模块最新内容
在项目中使用子模块的最简模型,就是只使用子项目并不时地获取更新,而并不在你的检出中进行任何更改。
# 想要在子模块中查看新工作,可以进入到目录中运行 git fetch 与 git merge。
$ git fetch
$ git merge origin/master
# 返回到主项目,查看子模块被更新的列表
git diff --submodule
上述,可以通过一种更简单的方式:Git将会进入子模块然后抓取并更新
$ git submodule update --remote t-module
注意:此命令默认会假定你想要更新并检出子模块仓库的master分支。不过你也可以设置为想要的其他分支。
# 设置从其他分支拉取代码
$ git config -f .gitmodules submodule.t-module.branch develop
$ git submodule update --remote
注意:如果不用 -f .gitmodules
选项,那么它只会为你做修改。但是在仓库中保留跟踪信息更有意义一些,因为其他人也可以得到同样的效果。
(5)在子模块与主项目中同时做修改
到目前为止,当我们运行 git submodule update
从子模块仓库中抓取修改时,Git将会获得这些改动并更新子目录中的文件,但是会将子仓库留在一个称作“游离的HEAD”的状态。这意味着没有本地工作分支(例如 “master”)跟踪改动。所以你做的任何改动都不会被跟踪。
$ git branch -a
首先,进入每个子模块并检出其相应的工作分支。接着,若你做了更改就需要告诉Git它该做什么,然后运行 git submodule update --remote
来从上游拉取新工作。你可以选择将它们合并到你的本地工作中,也可以尝试将你的工作变基到新的更改上。
$ git checkout master
$ git submodule update --remote --merge
# 可以让Git在推送到主项目前检查所有子模块是否已推送
$ git push --recurse-submodules=check
如果发现有未推送的文件,最简单的方式就是进入每一个子模块中然后手动推送到远程仓库。
(6)子模块技巧
有一个 foreach 子模块命令,它能在每一个子模块中运行任意命令。 如果项目中包含了大量子模块,这会非常有用。
# 保存所有子模块的工作进度
$ git submodule foreach ‘git stash‘
# 创建一个新分支,并将所有子模块都切换过去
$ git submodule foreach ‘git checkout -b featureA‘
(7)子模块的问题
问题一:在有子模块的项目中切换分支可能会造成麻烦
如果你创建一个新分支,在其中添加一个子模块,之后切换到没有该子模块的分支上时,你仍然会有一个还未跟踪的子模块目。
$ git checkout -b add-crypto
$ git submodule add https://github.com/chaconinc/CryptoLibrary
$ git commit -am ‘adding crypto library‘
$ git checkout master
$ git status
此时,会提示有未捕获的目录。
# 移除目录
$ git clean -fdx
当再次切回到有子模块的分支,需要重新初始化子模块
$ git checkout add-crypto
$ git submodule update --init
问题二:将子目录转换为子模块的问题
如果你在项目中已经跟踪了一些文件,然后想要将它们移动到一个子模块中,那么请务必小心。
# 必须要先取消暂存要转为子模块的目录,然后再将其添加为子模块
$ git rm -r CryptoLibrary
$ git submodule add https://github.com/chaconinc/CryptoLibrary
注意:这时如果尝试切换回的分支中那些文件还在子目录而非子模块中时,git会提示一个错误
$ git checkout master
error: The following untracked working tree files would be overwritten by checkout:
CryptoLibrary/Makefile
CryptoLibrary/includes/crypto.h
...
Please move or remove them before you can switch branches.
Aborting
你可以强制切换,但是要小心,如果其中还有未保存的修改,这个命令会把它们覆盖掉。
$ git checkout -f master
12. 打包
Git可以将它的数据“打包”到一个文件中。这在许多场景中都很有用。有可能你的网络中断了、你不在办公网中并且出于安全考虑没有给你接入内网的权限等等导致你无法push代码,但是你又想将你的代码共享给别人。这些情况下git bundle
就会很有用。
bundle命令会将git push
命令所传输的所有内容打包成一个二进制文件,你可以将这个文件通过邮件或者闪存传给其他人,然后解包到其他的仓库中。
(1)方式一:整个仓库打包
# 该文件包含了所有重建该仓库master分支所需的数据
$ git bundle create repo.bundle HEAD master
# 从文件中克隆出一个目录,就像从一个URL克隆一样
$ git clone repo.bundle repo
注意:如果你希望这个仓库可以在别处被克隆,你应该像上述例中那样增加一个HEAD引用
(2)方式二:仅仅打包变更的部分
我们继续在上述导出文件基础上生成的仓库中做两次提交。
步骤一:查看在我们的master分支而不在原始仓库中的提交
$ git log --oneline master ^origin/master
注意:图中提示信息“commit1”误写为了“commmit1”
步骤二:指定要打包的提交区间,并打包成指定文件名的文件
$ git bundle create commits.bundle master ^4df6152
注意:
- 4df6152为commit2前一次提交的ID,可以通过
git log
查看 - 可以将这个文件导入到原始的仓库中,即使在这期间已经有其他的工作提交到这个仓库中。
步骤三:将导出的文件通过邮件或者U盘传给别人
步骤四:获取文件中的内容
将接受到的文件,拷贝到和项目同目录下
# 检查这个文件是否是一个合法的Git包,是否拥有共同的祖先来导入
$ git bundle verify ../commits.bundle
# 查看这边包里可以导入哪些分支
$ git bundle list-heads ../commits.bundle
# 从包中取出master分支到我们仓库中的other-master分支
$ git fetch ../commits.bundle master:other-master
注意:上述不能合并到master分支
步骤五:剩下的工作,就只将other-master分支的内容合并到master分支上了
$ git merge --no-ff other-master
# 如果有冲突,可以手动解决冲突或者选择接受一方的提交
$ git checkout --ours/theirs test.js
13. 替换
Git对象是不可改变的,但它提供一种有趣的方式来用其他对象假装替换数据库中的Git对象。
replace命令可以让你在Git中指定一个对象并可以声称“每次你遇到这个Git对象时,假装它是其他的东西”。在你用一个不同的提交替换历史中的一个提交时,这会非常有用。
示例:
步骤一:假如拥有5个提交的简单仓库
$ git log --oneline
ef989d8 fifth commit
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
步骤二:将其分成拆分成两条历史
$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
步骤三:把这个新的history分支推送到我们新仓库的master分支
$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
步骤四:创建基础提交
$ echo ‘get history from blah blah blah‘ | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
注意:我们需要选择一个点去拆分,对于上例而言是第三个提交(SHA是 9c68fdc),因此我们的提交将基于此提交树。commit-tree
会返回一个全新的、无父节点的SHA提交对象。
步骤五:有一个基础提交,可以通过命令来将剩余的历史变基到基础提交之上
$ git rebase --onto 622e88 9c68fdc
到目前为止,我们已经用基础提交重写了最近的历史,基础提交包括如何重新组成整个历史的说明。我们可以将新历史推送到新项目中,当其他人克隆这个仓库时,他们仅能看到最近两次提交以及一个包含上述说明的基础提交。
如果,想获取整个项目的历史该如何做???
在克隆这个截断后的仓库后为了得到历史数据,需要添加第二个远程的历史版本库并对其做获取操作:
# 获取最新提交
$ git clone https://github.com/schacon/project
# 获取历史提交
$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
这样,在master分支中拥有最近的提交并且在project-history/master
分支中拥有过去的提交
# 最新提交
$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah
# 历史提交
$ git log --oneline project-history/master
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
最后一步:将master分支中的第四个提交替换为project-history/master分支中的“第四个”提交
$ git replace 81a708d c6e1e95
# 查看master分支中的历史信息
$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
综上所述,我们不用改变上游的SHA-1就能用一个提交来替换历史中的所有不同的提交,并且所有的工具(bisect,blame 等)也都可以使用!!!