• 约 8 分钟

精神状态:git原来是常用常新的吗?怎么突然冒出个cherry-pick啊

浅记录下最近才遇到要使用的git命令吧

worktree

之前开发节奏慢的时候,一般都是在同一个目录下哐当哐当干活,然后commit,完事了push,创PR,最多来个revert, rebase, merge都算复杂的了。可是,在有了AI之后,很多需求都是提的很急,开发节奏太快了,有时候甚至需要同时进行多个功能的开发,这种时候,AI在那边说“我太懂你的感受了”的时候,我一般会在旁边得闲饮茶先吗?nonono,当然是想着能不是同时开多个线程干活啦(后期请在此插入一头热爱工作的牛马.jpg)。但有时候啊有时候,会遇到两个功能需要在不同的环境/上下文下开发,也就是不同分支啦,那么怎么办呢,这个时候就要有请我们的git影分身之术——worktree登场啦。

worktree简单来说就是在本地创建了不同的目录,然后分别可以check到一个分支上,但worktree不能check到同一个分支,worktree是共享同一个.git目录的,新建worktree会一并携带所有被分支追踪的文件,也就是说像.env这样的文件不会一并本复制过去。.git文件存在的目录我叫它主worktree,至于你问我删除主worktree会怎样?这你还是问豆包吧,她比我懂。

然后下面是一些常用命令:

创建新鲜的worktree

git worktree add <目标目录> [分支名]

列出所有worktrees

git worktree list

删除worktree

git worktree remove <绝对路径>

不想让人知道的小秘密?试试这些!

我们从学走路的那天开始就被传授了.gitignore的使用方法,这个文件可以让我们忽略一些不必要提交的内容,但这有时候还是稍显乏力的,看看下面这几种情况:

已经追踪的文件,怎么忽略?

这里就得先泼一盆冷水了:.gitignore只能拦住还没被追踪的新文件,对于已经进过历史的文件,你把它加进.gitignore,Git只会翻个白眼继续追踪,毕竟人家都拿到编制了,哪能你说开就开。

既然不能直接封杀,那就先“劝退”再拉黑:

把文件从追踪列表里移除,但保留本地文件(--cached就是保命的关键):

git rm --cached <文件名>

提交之后它就恢复自由身了,这时再写进.gitignore就能正常拦截。

不想改.gitignore,要怎么忽略?

有时候你只是想在自己电脑上悄悄忽略几个东西——比如只会出现在你这台机器上的临时文件、个人的编辑器配置啥的——但这点私人恩怨实在不好意思写进团队的.gitignore里恶心大家。别急,Git偷偷留了个后门:

.git/info/exclude

打开一看,语法和.gitignore一模一样,写进去的规则只在本地生效,永远不会被提交到仓库里。它就是属于你一个人的.gitignore,低调、隐秘、不影响队友,简直是社恐福音。

废话开始(快进到废话结束)

哦,对了。(这一段有很多废话,请在家长陪同下阅读或酌情跳过)突然想到个事儿,就是吧,诶啧啧,咋说呢,嗨,好吧,我行我说会儿啊,嗯好,我呢,之前就那个啥,有个项目套娃嘛不是,哦更专业一点说应该是submodule,来,我吧,在这个submodule里边儿想要修改.git/info/exclude,嘿~您猜怎么着

答案揭晓:我兴冲冲地点开那个.git,发现它压根儿就不接待我——那根本不是一个目录,只是一个文件,里面孤零零躺着一行字:

gitdir: ../../.git/modules/my-submodule

一个平平无奇的路径,却像一张抄着远方地址的字条。得,本来只想安安静静改个文件,现在被迫开启寻址之旅了。既然都这样了,那就请各位系好安全带,本博客年度文学栏目正式开播:

《门后》

那年他接到任务:去修改一座城市的名册。地图上标得清清楚楚——就在城墙边上,门牌号.git。

可他到了地方才发现,那里根本没有门,只有一块薄薄的牌子,上面抄着一个远方的地址。

行吧,寻人启事变成了寻址启事。他按图索骥,穿过一层又一层的目录,像剥一颗不知道有没有芯的洋葱,每推开一扇门都在心里祈祷:这次总该是真的了吧?

终于,最后一扇门后面不再有字条,而是一座安静的地下城:objects是一座按内容寻址的巨型仓库,同样的记忆全宇宙只存一份;refs是一排指向不同时间线的路标;logs收着全城的监控录像带。而那本名册info/exclude,正安静地摊在角落的小桌上,扉页写着一行小字:

“本册只在本地生效,永不外传。”

改完名册离开时,他忍不住问守门人:为什么要把城的真实地址写在一张纸条上?

守门人说:孩子,城是活的呀。它哪天搬家了,也只需要换一张纸条而已。

——好,文学环节到此为止,观众请有序退场,接下来是人话总结时间。说人话就是:submodule里的.git不是目录而是个文件,里面用一行gitdir:记录了真身的位置,我的名册其实一直好好住在主仓库的.git/modules下面。跟这种“门后只有纸条”的迷宫斗智斗勇纯属浪费生命,不如直接让Git亲自带路:

cd $(git rev-parse --git-dir)

rev-parse --git-dir会现场算出当前仓库.git真身的所在位置,不管你在第几层子目录里摸鱼,是在worktree分身里打工,还是套娃套到怀疑人生的submodule里迷路,这条命令都能把你准确空降到你想去的那座城门口,从此告别对着一堆相对路径玩解谜的玄学生活。

废话结束

而如果连这么一个文件都不想留——毕竟它虽然不会被提交,但明晃晃躺在.git目录里,谁翻谁知道——那还有更阴险的:跳过一切配置文件,直接对着索引下手:

git update-index --skip-worktree <文件名>

这招等于给文件贴上“我的版本我说了算”的封条:不管你在本地怎么改,status和diff都会假装无事发生。不过远端哪天真更新了这个文件,pull的时候它会当场翻脸,到时候用--no-skip-worktree再把人家请回来就好。

比它还要再阴险一档的,是它的兄弟命令--assume-unchanged:

git update-index --assume-unchanged <文件名>

效果立竿见影:你随便折腾这个文件,Git全程睁一只眼闭一只眼。但这招本意是个性能优化工具,告诉Git“这种大文件就别费劲检查了”,所以承诺非常轻飘飘——切换分支、rebase之类的操作都可能让它悄悄失效,等哪天你发现“我明明改了代码怎么一点反应都没有”的时候,恭喜你解锁了排查半天的灵异事件。

顺手记个查案技巧,怀疑有文件在装死的时候:

git ls-files -v | grep '^[a-z]'

列出来的都是被开了装的选手,事后记得用--no-skip-worktree或--no-assume-unchanged把它们一个个捞回来,别让它们永远活在平行时空里。

发明天才的人真是个cherry-pick

回到开头的故事:多线程干活的时候,另一个分支上恰好有一颗commit是你当前分支急缺的灵丹妙药,merge会把人家一整桌菜都端过来,rebase又动静太大,可你只是想吃那一颗樱桃而已。这时候cherry-pick就像自助餐里的夹子:对不起,我只夹这一块。

它的作用简单粗暴:把指定的commit“复制”一份应用到当前分支,并生成一个崭新的commit。注意是复制不是搬家,原分支上的那颗樱桃还完好无损。

摘一颗尝尝:

git cherry-pick <commit-hash>

只想拿内容不要新的commit记录(应用后停在暂存区):

git cherry-pick -n <commit-hash>

一次夹一把(A^..B表示含A到B的连续区间):

git cherry-pick <hash1> <hash2> <hash3>
git cherry-pick A^..B

当然,天上不会掉好摘的樱桃,冲突该来还是会来,处理完之后:

git cherry-pick --continue   # 解决了,继续摘下一颗
git cherry-pick --abort      # 溜了溜了,当无事发生

所以说常用常新,以前觉得这辈子都用不上它,现在天天开口就是“这颗熟了我先摘走了啊?”

林威
林威 咖味十足的软件工程师