Git 进阶使用1
前言
Git 作为全球软件开发者的标配代码管理工具,是程序员离不开的日常伙伴,除了基本的几条命令外,git其实还有很多日常会用到的option,这里以我的个人经验做个总结
Git Merge vs Git rebase
我们要知道的第一件事是,git rebase
和git merge
做的事其实是一样的;它们都被设计来将一个分支的更改并入另一个分支,只不过方式有些不同
想象一下,你刚创建了一个专门的分支开发新功能,然后团队中另一个成员在 master 分支上添加了新的提交。这就会造成提交历史被 fork 一份,用 Git 来协作的开发者应该都很清楚
现在,如果 master 中新的提交和你的工作是相关的;为了将新的提交并入你的分支,你有两个选择:merge 或 rebase
Merge
将 master 分支合并到 feature 分支最简单的办法就是用下面这些命令:
1 | git checkout feature |
或者,你也可以把它们压缩在一行里
1 | git merge master feature |
最终创建出这样的 workflow:
Rebase
作为 merge 的替代选择,你可以像下面这样将 feature 分支并入 master 分支:
1 | git checkout feature |
它会把整个 feature 分支移动到 master 分支的后面,有效地把所有 master 分支上新的提交并入过来。但是,rebase 为原分支上每一个提交创建一个新的提交,重写了项目历史,并且不会带来合并提交
rebase最大的好处是你的项目历史会非常整洁。首先,它不像 git merge
那样引入不必要的合并提交。其次,如上图所示,rebase 导致最后的项目历史呈现出完美的线性——你可以从项目终点到起点浏览而不需要任何的 fork。这让你更容易使用 git log
、git bisect
和 gitk
来查看项目历史
rebase 创建出的 workflow 是像这样的:
Rebase的黄金准则
首先一定要注意的一点是:永远不要在公共/共享的分支上使用git rebase
(尤其是对于主干分支保护不好的仓库); rebase
和merge
最大的一点不同就是作用的对象不同,rebase
是作用于"rebase on"的分支的,而merge
是作用于当前分支的
假设现在Bob和Anna在同一个项目组中工作,项目所属的仓库和分支大概是下图这样:
现在Bob为了图一时方便打破了原则,正巧这时Anna在特征分支上进行了新的提交,此时的结构图大概是这样的:
当Bob推送自己的分支到远端的时候,现在的分支情况如下:
然后呢,当Anna也进行推送的时候,她会得到如下的提醒,Git提醒Anna她本地的版本与远程分支并不一致,需要向远端服务器拉取代码进行同步:
在Anna提交之前,分支中的Commit序列是如下这样的:
1 | A--B--C--D' origin/feature // GitHub |
在进行Pull操作之后,Git会进行自动地合并操作,结果大概是这样的:
这个第M个提交即代表着合并的提交,也就是Anna本地的分支与Github上的特征分支最终合并的点,现在Anna解决了所有的合并冲突并且可以Push她的代码,在Bob进行Pull之后,每个人的Git Commit结构为:
看到上面这个混乱的流线图,相信你对于Rebase和所谓的黄金准则也有了更形象深入的理解
假设下还有一哥们Emma,第三个开发人员,在他进行了本地Commit并且Push到远端之后,仓库变为了:
- 这还只是仅有几个人,一个特征分支的项目因为误用rebase产生的后果。如果你团队中的每个人都对公共分支进行rebase操作,那么后果就是乱成一片
- 另外,相信你也注意到,在远端的仓库中存有大量的重复的Commit信息,这会大大浪费我们的存储空间
- 因此,**不能在一个共享的分支上进行Git rebase操作,避免出现项目分支代码提交记录错乱和浪费存储空间的现象
总结
- 融合代码到公共分支的时使用
git merge
,而不用git rebase
- 融合代码到个人分支的时候使用
git rebase
,可以不污染分支的提交记录,形成简洁的线性提交历史记录
Git submodule
当你在一个Git 项目上工作时,你需要在其中使用另外一个Git 项目。也许它是一个第三方开发的Git 库或者是你独立开发和并在多个父项目中使用的。这个情况下一个常见的问题产生了:你想将两个项目单独处理但是又需要在其中一个中使用另外一个
在Git 中你可以用子模块submodule
来管理这些项目,submodule
允许你将一个Git 仓库当作另外一个Git 仓库的子目录。这允许你克隆另外一个仓库到你的项目中并且保持你的提交相对独立
添加子模块
此文中统一将远程项目https://github.com/maonx/vimwiki-assets.git
克隆到本地assets
文件夹
1 | git submodule add https://github.com/maonx/vimwiki-assets.git assets |
添加子模块后运行git status
, 可以看到目录有增加1个文件.gitmodules
, 这个文件用来保存子模块的信息
1 | git status |
查看子模块
1 | git submodule |
更新子模块
更新项目内子模块到最新版本
1
git submodule update
更新子模块为远程项目的最新版本
1
git submodule update --remote
克隆包含子模块的项目
克隆包含子模块的项目有二种方法:一种是先克隆父项目,再更新子模块;另一种是直接递归克隆整个项目。
克隆父项目,再更新子模块
克隆父项目
1 | git clone https://github.com/maonx/vimwiki-assets.git assets |
查看子模块
1 | git submodule |
子模块前面有一个-
,说明子模块文件还未检入(空文件夹)。
初始化子模块
1 | git submodule init |
初始化模块只需在克隆父项目后运行一次
更新子模块
1 | git submodule update |
递归克隆整个项目
1 | git clone https://github.com/maonx/vimwiki-assets.git assets --recursive |
递归克隆整个项目,子模块已经同时更新了,一步到位
修改子模块
在子模块中修改文件后,直接提交到远程项目分支
1 | git add . |
删除子模块
删除子模块比较麻烦,需要手动删除相关的文件,否则在添加子模块时有可能出现错误
同样以删除assets
文件夹为例:
删除子模块文件夹
1 | git rm --cached assets |
删除.gitmodules
文件中相关子模块信息
1 | [submodule "assets"] |
删除.git/config
中的相关子模块信息
1 | [submodule "assets"] |
删除.git
文件夹中的相关子模块文件
1 | rm -rf .git/modules/assets |
Git cherry-pick & Git stash
cherry-pick
在我们使用Git进行日常开发的过程中,常常需要进行的操作就是代码合并了。常见的操做命令是 git merge branch-name
,这个命令会合并的是整个分支的commit
,然而有时候我们需要的可能是仅仅某一个 commit
或者某几个commit
,这时候就需要用到git cherry-pick
了
git cherry-pick
的作用就如它的名字一样,精心挑选。我们可以精心挑选其他分支上的 commit
合并到当前的分支上来
原理
git cherry-pick
可以把其他分支的某个commit
应用到当前分支,并且自动生成一个新的 commit
进行提交,因此这两次commit
的哈希值是不一样的,属于不同的commit
。
基本用法
单个commit
合并
1 | git cherry-pick commit-hash/branch-name |
如果使用的是哈希值,则会把对应的commit
合并过来,如果是分支名,则会把对应分支的**最新一次commit
**合并过来。
多个commit
合并
1 | 1、 分散的commit |
注意
无论是对单个 commit 进行 cherry-pick ,还是批量处理,注意一定要根据时间线,依照 commit 的先后顺序来处理,否则会有意想不到的问题。
如何处理冲突
代码合并不可避免的就是会遇到代码冲突了,git merge
会遇到冲突,同样的git cherry-pick
也会遇到代码冲突,那么遇到代码冲突的时候,该如何处理呢?
遇到冲突的时候,Git会给出报错信息,并停下来,要求用户解決 conflict 的问题。Git会把所有冲突的文件列在Unmerged paths
的地方,可以通过git status
查看,如下图
此时我们有以下处理方案:
- 解决冲突
- 修改冲突的地方,并通过命令
git add .
把文件重新加入暂存区。 - 继续合并,
git cherry-pick --continue
- 修改冲突的地方,并通过命令
- 回退所有修改:
git cherry-pick --abort
,此时会回到操作前的样子 - 单纯退出
cherry-pick
:git cherry-pick --quit
,此时不会回到操作前的状态
stash
当我们在需要紧急处理一个bug时,工作区内容又没有完成,不适合提交,这时候就需要使用git stash
暂存起来
git stash 命令获取我们未提交的更改(暂存和未暂存),将它们保存以备后用,然后从工作副本中恢复它们
1 | $ git status |
在我们完成了临时的工作后,我们可以使用git stash pop
命令重新应用之前暂存的更改
1 | $ git stash pop |
或者,我们可以使用 git stash apply
将之前隐藏的更改重新应用到工作副本,但是和 git stash pop
不同, 并不会将他们从stash中删除
1 | $ git stash apply |
Git revert & Git reset
revert
revert 会通过创建新的提交来撤消 commit,这是一种安全的撤消 commit 的方法,因为它是新建一个 commit 来逆向地恢复某一次commit;例如,下面的命令将找出倒数第二个 commit 中包含的更改,创建一个撤消这些更改的新提交,并将新提交附加到现有项目
1 | git revert HEAD~2 |
工作流将是现在这样:
对比与git reset,后者确实会改变现有的提交历史;因此,应该使用Git revert来撤消公共分支上的更改,git reset仅仅应该被用于撤消专用分支上的更改
Git hook
Git hook是仓库中特定事件发生时 Git 自动运行的普通脚本。因此,Git hook安装和配置也非常容易
hook在本地或服务端仓库都可以部署,且只会在仓库中事件发生时被执行
配置hook
hook存在于每个 Git 仓库的 .git/hooks
目录中。当你初始化仓库时,Git 自动生成这个目录和一些示例脚本。当你观察 .git/hooks
时,你会看到下面这些文件:
1 | applypatch-msg.sample pre-push.sample |
这里已经包含了大部分可用的hook了,但是 .sample
拓展名防止它们默认被执行。为了安装一个hook,你只需要去掉 .sample
拓展名。或者你要写一个新的脚本,你只需添加一个文件名和上述匹配的新文件,去掉 .sample
拓展名。
比如说,试试安装一个 prepare-commit-msg
hook。去掉脚本的 .sample
拓展名,在文件中加上下面这两行:
1 | #!/bin/sh |
hook需要能被执行,所以如果你创建了一个新的脚本文件,你需要修改它的文件权限。比如说,为了确保 prepare-commit-msg
可执行,运行下面这个命令:
1 | chmod +x prepare-commit-msg |
接下来你每次运行 git commit
时,你会看到默认的提交信息都被替换了。我们会在「准备提交信息」一节中细看它是如何工作的。现在我们已经可以定制 Git 的内部功能,你只需要坐和放宽。
内置的样例脚本是非常有用的参考资料,因为每个hook传入的参数都有非常详细的说明(不同hook不一样)
hook的作用域
对于任何 Git 仓库来说hook都是本地的,而且它不会随着 git clone
一起复制到新的仓库。而且,因为hook是本地的,任何能接触得到仓库的人都可以修改。
对于开发团队来说,这有很大的影响。首先,你要确保你们成员之间的hook都是最新的。其次,你也不能强行让其他人用你喜欢的方式提交——你只能鼓励他们这样做。
在开发团队中维护hook是比较复杂的,因为 .git/hooks
目录不随你的项目一起拷贝,也不受版本控制影响。一个简单的解决办法是把你的hook存在项目的实际目录中(在 .git
外)。这样你就可以像其他文件一样进行版本控制。为了安装hook,你可以在 .git/hooks
中创建一个符号链接,或者简单地在更新后把它们复制到 .git/hooks
目录下
本地hook
本地hook只影响它们所在的仓库。因为开发者可以修改他们本地的hook,所以不要用它们来推行强制的提交规范。不过,它们确实可以让开发者更易于接受这些规范
常见的 6 个最有用的本地hook:
- pre-commit
pre-commit
脚本在每次你运行git commit
命令时,Git 向你询问提交信息或者生产提交对象时被执行 - prepare-commit-msg
prepare-commit-msg
hook在pre-commit
hook在文本编辑器中生成提交信息之后被调用 - commit-msg
commit-msg
hook和prepare-commit-msg
hook很像,但它会在用户输入提交信息之后被调用。这适合用来提醒开发者他们的提交信息不符合你团队的规范 - post-commit
post-commit
hook在commit-msg
hook之后立即被运行 。它无法更改git commit
的结果,所以这主要用于通知用途 - post-checkout
post-checkout
hook和post-commit
hook很像,但它在你用git checkout
查看引用的时候被调用。这是用来清理你的工作目录中可能会令人困惑的生成文件 - pre-rebase
pre-rebase
hook在git rebase
发生更改之前运行,确保不会有什么糟糕的事情发生
前四个hook让你介入完整的提交生命周期,后两个允许你执行一些额外的操作,分别为 git checkout
和 git rebase
的安全检查。
所有带 pre-
的hook允许你修改即将发生的操作,而带 post-
的hook只能用于通知
服务端hook
服务端hook和本地hook几乎一样,只不过它们存在于服务端的仓库中(比如说中心仓库,或者开发者的公共仓库)。当和官方仓库连接时,其中一些可以用来拒绝一些不符合规范的提交。
常用的 3 个服务端hook:
- pre-receive
pre-receive
hook在有人用git push
向仓库推送代码时被执行。它只存在于远端仓库中,而不是原来的仓库中 - update
update
hook在pre-receive
之后被调用,它可以分别被每个推送上来的引用分别调用 - post-receive
post-receive
hook在成功推送后被调用,适合用于发送通知
这些hook都允许你对 git push
的不同阶段做出响应
服务端hook的输出会传送到客户端的控制台中,所以给开发者发送信息是很容易的。但要记住这些脚本在结束完之前都不会返回控制台的控制权,所以要小心那些长时间运行的操作
Git config
多用户
单台机器单个服务多用户的场景下,由于服务商通常不会允许同一对密钥认证多个用户,所以需要生成多对密钥,这时就需要指定Git
使用的用户以及对应的私钥文件了
1 | git config --local user.name "xxx" |
注意加上--local
,指定仅当前仓库生效
Git Sparse Checkout
很多时候我们只需要一个大型仓库中的个别文件或是文件夹,而不是直接 clone 整个仓库,这时候就需要用到sparse checkout
对于git 2.19以上的版本,我们可以使用git clone --filter
来首先避免在 git clone 时下载对象
比如针对这样一个示例仓库,仓库中的内容为:
- 包含10个10MB文件的大目录
- 包含1000个1B大小文件的小目录
我们就可以使用以下方式仅仅获取到 small 目录中的内容
1 | git clone \ |
vscode git插件推荐
- git-commit-plugin
模版化 commit message 插件 - Git Graph
Git workflow 图形化插件 - GitLens
综合性的 Git 插件,覆盖大部分功能