作为程序员的我们应该都有一个感受,一旦进入某个项目,从开发,到发布生产,到hotfix,到后期维护,那基本都有你的份,正在开发某个feature,老板突然跳出来说让你做生产商的hotfix更是家常便饭,面对这种情况,使用Git的我们通常有两种解决方案:
- 草草提交未完成的feature,然后切换分支到hotfix
- gitstash|gitstashpop暂存工作内容,然后再切换到hotfix
第二种方式较第一种还好很多,可是面对下面这些场景,stash依旧不是很好的解决方案
我们面对的场景
- 正在main分支上跑很长时间的测试,切换到hotfix或feature,测试就会中断
- 项目非常大,频繁地切换索引,成本非常高
- 在几年前release的旧版本,设置和当前不一样,IDErestructure适配切换也会带来很大的开销
- 切换分支,需要重新设置相应的环境变量,比如dev/qa/prod
- 需要切换到同事的代码,帮助调试代码复现问题
有的同学想到,gitclone多个repo不就可以了吗?这是解决上述问题的一个方法,但背后同样隐藏着很多问题:
- 多个repo的状态是不好同步的,比如没办法快速cherry-pick,一个repocheckout的分支,另外一个repo需要重新checkout
- githistory/log是重复的,当项目历史非常长,.git文件夹下的内容是非常占用磁盘空间的
- 同一个项目,多个repo,不易管理
那如何做才能满足这些特殊场景,又不出现上述这些问题呢?
git-worktree
其实,这是Git2015年前就开始支持的功能,却很少有人知道它,git-worktree的使用非常方便,在终端输入:
gitworktree--help
就可以快速看到帮助文档说明:
用简单的话来解释git-worktree的作用就是:
仅需维护一个repo,又可以同时在多个branch上工作,互不影响
上面红色框线命令有很多,我们常用的其实只有下面这四个:
gitworktreeadd[-f][--detach][--checkout][--lock][-b<new-branch>]<path>[<commit-ish>]gitworktreelist[--porcelain]gitworktreeremove[-f]<worktree>gitworktreeprune[-n][-v][--expire<expire>]
在展开说明之前,需要和大家普及两个你可能忽视的Git知识点:
- 默认情况下,gitinit或gitclone初始化的repo,只有一个worktree,叫做mainworktree
- 在某一个目录下使用Git命令,当前目录下要么有.git文件夹;要么有.git文件,如果只有.git文件,里面的内容必须是指向.git文件夹的
第二句话感觉挺绕的,下面用例子说明,就很容易明白了
gitworktreeadd
当前项目目录结构是这样的(amend-crash-demo是repo的root):
.└──amend-crash-demo1directory
cdamend-crash-demo运行命令gitworktreeadd../feature/feature2
➜amend-crash-demogit:(main)gitworktreeadd../feature/feature2Preparingworktree(newbranch'feature2')HEADisnowat82b8711addmainfile
重新看目录结构
.├──amend-crash-demo└──feature└──feature23directories
该命令默认会根据HEAD所在的commit-ish(当然也可以指定gitlog中的任意一个commit-ish)创建一个名为feature2的分支,分支磁盘位置如上面结构所示
cd../feature/feature2/会发现,这个分支下并不存在.git文件夹,却存在一个.git文件,打开文件,内容如下:
gitdir:/Users/rgyb/Documents/projects/amend-crash-demo/.git/worktrees/feature2
到这里,你再理解一下上面的知识点2,是不是就清晰许多了呢?
接下来,你就可以在feature2分支上做一切你想做的内容了(add/commit/pull/push),和mainworktree互不干扰
一般情况下,项目组都有一定的分支命名规范,比如feature/JIRAID-Title,hotfix/JIRAID-Title,如果仅仅按照上面命令新建worktree,分支名称中的/会被当成文件目录来处理
gitworktreeadd../hotfix/hotfix/JIRA234-fix-naming
运行完该命令,文件目录结构是这样的
.├──amend-crash-demo├──feature│└──feature2└──hotfix└──hotfix└──JIRA234-fix-naming6directories
很显然这不是我们想要的,这时候我们就需要-b参数的支持了,就像gitcheckout-b一样
执行命令:
gitworktreeadd-b"hotfix/JIRA234-fix-naming"../hotfix/JIRA234-fix-naming
再来看一下目录结构
.├──amend-crash-demo├──feature│└──feature2└──hotfix├──JIRA234-fix-naming└──hotfix└──JIRA234-fix-naming7directories
进入JIRA234-fix-naming目录,默认是在hotfix/JIRA234-fix-naming分支上
worktree建立起来很容易,不加管理,项目目录结构肯定乱糟糟,这是我们不想看到的,所以我们需要清晰地知道某个repo都建立了哪些worktree
gitworktreeadd[-f][--detach][--checkout][--lock][-b<new-branch>]<path>[<commit-ish>]gitworktreelist[--porcelain]gitworktreeremove[-f]<worktree>gitworktreeprune[-n][-v][--expire<expire>]0
所有的worktree都在共用一个repo,所以在任意一个worktree目前,都可以执行如下命令来查看worktree列表
gitworktreeadd[-f][--detach][--checkout][--lock][-b<new-branch>]<path>[<commit-ish>]gitworktreelist[--porcelain]gitworktreeremove[-f]<worktree>gitworktreeprune[-n][-v][--expire<expire>]0
执行完命令后,可以查看到我们上面创建的所有worktree信息,mainworktree也会显示在此处
gitworktreeadd[-f][--detach][--checkout][--lock][-b<new-branch>]<path>[<commit-ish>]gitworktreelist[--porcelain]gitworktreeremove[-f]<worktree>gitworktreeprune[-n][-v][--expire<expire>]1
worktree工作做完了,也是要及时删除的,否则也会浪费很多磁盘空间
gitworktreeremove
这个命令很简单了,worktree的名字叫什么,直接就remove什么就好了
gitworktreeadd[-f][--detach][--checkout][--lock][-b<new-branch>]<path>[<commit-ish>]gitworktreelist[--porcelain]gitworktreeremove[-f]<worktree>gitworktreeprune[-n][-v][--expire<expire>]2
此时,分支名弄错的那个hotfix就被删掉了
gitworktreeadd[-f][--detach][--checkout][--lock][-b<new-branch>]<path>[<commit-ish>]gitworktreelist[--porcelain]gitworktreeremove[-f]<worktree>gitworktreeprune[-n][-v][--expire<expire>]3
假设你创建一个worktree,并在里面有改动,突然间这个worktree又不需要了,此刻你按照上述命令是不能删掉了,此时就需要-f参数来帮忙了
gitworktreeadd[-f][--detach][--checkout][--lock][-b<new-branch>]<path>[<commit-ish>]gitworktreelist[--porcelain]gitworktreeremove[-f]<worktree>gitworktreeprune[-n][-v][--expire<expire>]4
删除了worktree,其实在Git的文件中,还有很多administrative文件是没有用的,为了保持清洁,我们还需要进一步清理
gitworktreeprune
这个命令就是清洁的兜底操作,可以让我们的工作始终保持整洁
总结
到这里,你应该理解,整个git-worktree使用流程就是下面这四个命令:
gitworktreeaddgitworktreeadd[-f][--detach][--checkout][--lock][-b<new-branch>]<path>[<commit-ish>]gitworktreelist[--porcelain]gitworktreeremove[-f]<worktree>gitworktreeprune[-n][-v][--expire<expire>]0gitworktreeremovegitworktreeprune
你也应该明白gitworktree和gitclone多个repo的区别了。只维护一个repo,创建多个worktree,操作间行云流水
我的实践:通常使用gitworktree,我会统一目录结构,比如feature目录下存放所有feature的worktree,hotfix目录下存放所有hotfix的worktree,这样整个磁盘目录结构不至于因为创建多个worktree而变得混乱
在磁盘管理上我有些强迫症,理想情况下,某个repo的worktree最好放在这个repo的文件目录里面,但这就会导致Gittrack新创建的worktree下面的所有文件,为了避免Gittrackworktree的内容,来来回回修改gitignore文件肯定是不合适的,