6

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 界面一样,在终端中查看不同分支的提交信息。
git-log.png
(这里采用了一个开源项目的提交历史来示例,如果是公司的商业项目,输出会更炫目)

这个输出跟 man xxx 一样,会通过 $PAGER(通常是 less) 显示在终端上。这意味着你可以在输出中搜索特定的提交或 tag。
许多 GUI 界面并不提供这一功能,导致用户只能瞪大眼睛,一点点地挪动,才能揪出特定的提交。
就这一点上,终端版的功能要比 GUI 版本胜出许多。
终端版的另一个优点是,提供了自由组合各种选项的能力。你可以自定义输出信息和显示的提交范围。
如果嫌命令太长,可以给它一个 alias(通过 git alias 或 shell 的 alias 功能)。

git cherry

有一个常用的 Git 操作是 git cherry-pickgit 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,不能不提误操作时的补救办法。

  1. 被抹掉的内容已经提交了。这种情况好办。通过 git reflog,我们可以找到被抹掉的提交,然后 cherry-pick回来,抑或 reset --hard HEAD{x} 到对应的提交上去。

  2. 被抹掉的内容原来在暂存区里。这个比较蛋疼。你需要 git fsck | grep blob 拣出每一个 dangling blob,逐个 git show xxx 看看,应该能找回被抹掉的内容。这种感觉,就像在垃圾桶里翻出丢失的手稿一样。

  3. 被抹掉的内容不在暂存区里。靠 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 <file>..." to update what will be committed)
  (use "git checkout -- <file>..." 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 <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   check_integrity.sh

spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.