Git 非常复杂。我即使几乎每天都在用,也不敢保证对 Git 的每一个角落都了如指掌。
本文试图盘点一些不知名,但在我看来较常用的操作。如果没有你预期的操作,说明要么在我看来它很知名,要么在我看来它不太常用。
git log --graph --abbrev-commit --decorate --all --oneline
git cherry
git show ref:path
git reset [--hard|--soft|--keep]
git update-index --assume-unchanged
git log --graph --abbrev-commit --decorate --all --oneline
如果要看项目中的分支情况,通常的做法是打开一个 Git GUI 软件,通过 GUI 界面来查看全景。
对于有些人来说,这是装 GUI 的唯一需求。
其实这个需求可以砍掉,因为 Git 提供了在终端中输出提交记录全景的操作:
git log --graph --abbrev-commit --decorate --all --oneline
输出结果相当惊艳,你可以像看 GUI 界面一样,在终端中查看不同分支的提交信息。
(这里采用了一个开源项目的提交历史来示例,如果是公司的商业项目,输出会更炫目)
这个输出跟 man xxx
一样,会通过 $PAGER(通常是 less) 显示在终端上。这意味着你可以在输出中搜索特定的提交或 tag。
许多 GUI 界面并不提供这一功能,导致用户只能瞪大眼睛,一点点地挪动,才能揪出特定的提交。
就这一点上,终端版的功能要比 GUI 版本胜出许多。
终端版的另一个优点是,提供了自由组合各种选项的能力。你可以自定义输出信息和显示的提交范围。
如果嫌命令太长,可以给它一个 alias(通过 git alias 或 shell 的 alias 功能)。
git cherry
有一个常用的 Git 操作是 git cherry-pick
。git cherry
顾名思义,自然跟 cherry-pick
有关。git cherry
可以比较两个分支,筛选出存在于甲分支却不存在于乙分支的提交。显而易见,这是在为后面的 cherry-pick
做准备。
¥ git cherry 4.2.0 | tail
+ bb5f4b02340b3c269d7a8e11842d4a16b2b8eba1
+ 2451c0c202dd48bc610ef9731c503ea54f19af83
+ 64b4a599e8af4b31444f2a86559d4860401925f4
+ c80b4e773f746ffe11aca9010b4433f6b6ba13ad
+ b2c7de9a5a5c8ea0254001a07ae24ffc27118dbb
+ 719dd2c3af9ba801114101ccdec1cd9e2f1bbc5d
+ 518db6e648aaf646db4be3067a583bdcc37ee2a8
+ b01359efc5761a17cd3b893fd51219a005009641
+ 422c9a8e69acc5cf013340715f8d2fa7d7cabf75
+ cc0dff2f49cfbd786eca498fe2532de0455e1759
美中不足的是,git cherry
的输出是 sha1,对机器来说友好,对人来说就是天书。如果要看具体的提交信息,需要借其他命令之力。比如 git show
:
git show --no-patch --format='%s' cc0dff2f49cfbd786eca498fe2532de0455e1759
git show ref:path
git show
是我最喜欢的命令之一。输出提交的内容,是 git show
的拿手好戏之一。举个例子,git show @
可以显示当前分支上最新提交。(@
是 HEAD
的别名,能少敲三个字符呢)
不过这不算 git show
的杀手锏。git show
的绝活,是能在不检出特定提交的前提下,显示特定提交的某个文件的内容。
假设我们想看看 xxx 提交上的 db.lua
,纯粹只想看一看,不涉及调试或其他复杂的操作。我们当然可以 git checkout xxx
,然后想看啥就看啥。然而用 git show
的话,就能保持当前分支不变,直接显示 xxx 提交上的 db.lua
了:
git show xxx:path/to/db.lua
编辑器的 Git 插件一般支持在编辑器内直接打开 git show
的输出。如果你用的是 Vim,又装了 fugitive,
通过敲 :Gedit revision:%
,可以打开当前文件的特定版本。
如果想找到哪几个提交修改了特定文件,可以先用 git log --format='%s' -- /path/to/your/file
获取涉及的提交。
git reset [--hard|--soft|--keep]
git reset
可不能算是“不知名”操作。任何 Git 入门教程,都会提到怎么用 git reset
把文件移出暂存区。但在本人的日常开发流程中,移出暂存区其实是最少用上的git reset
操作。所以我觉得可以打个擦边球,讲讲 git reset
其他的打开方式。
移出暂存区的 git reset
操作,相当于 git reset --mixed
。除了 --mixed
,常用的选项还有下面三个:
--soft
--hard
--keep
开发中常有的情况:劈劈啪啪写了一堆代码,连续好几个提交。然后脑子里灵机一动,顿悟了更好的做法。这个做法是如此地好,以致于之前的提交记录都成了你的黑历史。你恨不得
把这些提交记录一笔勾销,但该怎么办呢?这时候就轮到 reset --soft
上场了。reset --soft
俗称 squash
,可以在保留修改的同时,把最近的几个提交历史给抹掉。比如 git reset --soft @~3
就能重置到倒数第四个提交,但保留被抹掉的三个提交的更改。
这么操作之后再提交一次,然后选一个夜深人静的时刻,push -f
到远程仓库,你的黑历史就烟消云散了。当然还是会留下些痕迹什么的,但至少别人一般发现不了……
跟 --soft
对应的是 --hard
。--hard
也会抹掉最近的几个提交,但不保留修改。如果感到自己的代码实在没法看,可以直接 reset --hard
试试。不过 reset --hard
倒不失清理环境的好方法。
--keep
正好在 --soft
和 --hard
之间。--keep
会保留未提交的更改,这一点跟 --soft
一样;同时会抹掉已经提交的更改,这一点跟 --hard
相同。
有些时候,这一特性会派上用场。
如果你在开发的时候,突然发现自己忘记切分支了,可以这么做:
git branch feature/you_need
git reset --keep comit_your_hack_starts_from
git checkout feature/you_need
这样做既消除了错误分支上的提交,又保留了工作区里未提交的内容。
提到 reset
,不能不提误操作时的补救办法。
被抹掉的内容已经提交了。这种情况好办。通过
git reflog
,我们可以找到被抹掉的提交,然后cherry-pick
回来,抑或reset --hard HEAD{x}
到对应的提交上去。被抹掉的内容原来在暂存区里。这个比较蛋疼。你需要
git fsck | grep blob
拣出每一个 dangling blob,逐个git show xxx
看看,应该能找回被抹掉的内容。这种感觉,就像在垃圾桶里翻出丢失的手稿一样。被抹掉的内容不在暂存区里。靠 Git 已经救不回来了。要不找下编辑器的备份文件,要不尝试能不能从文件系统缓存里把文件抠出来。如果不行,试试磁盘数据恢复程序……
说这么多,关键记住一点:reset 有风险,用时需谨慎。
git update-index --assume-unchanged
假设代码仓库里存在这么一个配置文件:每个人在开发时,会根据本地情况修改里面的值。比如某些 IDE 的项目描述文件,里面可能夹杂着项目的路径。
有什么办法,可以避免本地的修改不会提交到仓库里去呢?
个人认为 git update-index --assume-unchanged
是众多解决方法中最好的。git update-index
可以修改 Git 对特定路径下的文件的处理方式。其中 --assume-unchanged
选项告诉 Git,指定路径下的文件不会有修改。
即使在本地修改了那些文件,也不会被 Git 记录在案。
如果我有一天需要修改这个配置文件并入库,可以用 --no-assume-unchanged
选项解开封印,让 Git 重新追踪修改。
¥ gs
On branch master
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git checkout -- ..." to discard changes in working directory)
modified: check_integrity.sh
no changes added to commit (use "git add" and/or "git commit -a")
¥ git update-index --assume-unchanged check_integrity.sh
¥ gs
On branch master
nothing to commit, working tree clean
¥ git update-index --no-assume-unchanged check_integrity.sh
¥ gs
On branch master
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git checkout -- ..." to discard changes in working directory)
modified: check_integrity.sh