Git 学习笔记(CheatSheet)(二)

2021-01-13  本文已影响0人  Whyn
Git 思维导图

高级技巧

下面介绍几个 Git 中非常强大的命令,借助这些命令我们可以完成一些非常有用的操作。

git reflog

Git 对于本地仓库的操作都进行了日志记录,日志文件存储在目录.git/logs

$ tree .git/logs
.git/logs
├── HEAD             # HEAD 指针变更记录
└── refs
    ├── heads        # 本地分支记录
    │   ├── master
    ├── remotes      # 远程分支记录
    │   └── origin
    │       └── HEAD
    │       └── master
    └── stash        # stash 记录

从 Git 的日志目录中可以看出,Git 主要对一些引用文件进行了日志记录,包括HEAD指针、分支和储藏更改记录。当本地更改了这些指针指向时,该操作就会被记录到对应的日志文件中。比如,切换分支会导致HEAD指针指向变化,则该操作会被记录到日志文件.git/logs/HEAD中,比如,master分支添加或删除commit时会同时导致master指针和HEAD指针指向变化,则该操作会同时被记录到.git/logs/refs/heads/master.git/logs/HEAD日志文件中...

由于日志记录了所有指针、分支和储藏的变更操作,因此,本地仓库的所有提交都不会丢失,可随时从这些日志文件中索取得回。比如,假设我们在当前分支指向了回退操作,那么当前分支的日志记录git log会丢失被回退的那些提交,如果想找回这些提交,搜索该分支日志文件即可。当然,实际操作中,我们无需手动查询这些日志文件(虽然这些日志文件都是文本文件),因为 Git 提供了相关命令可以让我们直接查询相应日志记录,该命令为git reflog,其具体语法如下所示:

# 查询日志
git reflog [show] [<ref>]

# 删除过期日志
git reflog expire [<ref>]

# 删除日志条目
git reflog delete ref@{specifier}

# 检测引用是否存在日志文件
git reflog exists <ref>

大多数情况下我们都是使用git reflog [show]来查看日志记录,因此下面我们具体介绍常用的一些查询操作:

最后,日志文件只存在于本地仓库中,不会上传到远程仓库,并且,日志条目默认只有 90 天有效期限,过期条目可能会被自动删除(因为某些命令会触发git gc操作),也可以通过手动执行git gcgit reflog expire删除过期条目。如果想更改日志条目有效期限,可以设置gc.reflogExpire配置或执行git reflog expire --expire=<time>手动指定时间。比如,下面的命令将删除 1 分钟之前的所有日志条目:

# --all 表示清除所有日志,也可以指定清除特定日志文件,比如 HEAD、master...
$ git reflog expire --expire=1.minute.ago --all

git rebase

git rebase是 Git 提供的一个具备非常强大功能的命令,rebase中文翻译为『变基』,见名知意,即git rebase可以更改基准点,比如,一个分支从另一个分支某个提交上创建,使用git rebase可以更改该分支基准点,使新分支从另一个提交上创建延伸,扩展来说,git rebase不仅仅可以用于修改分支基准点,它还具备修改分支提交历史记录,比如删除、合并、更换提交...

git rebase的具体语法如下所示:

git rebase [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase> | --keep-base] [<upstream> [<branch>]]
git rebase [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>] --root [<branch>]
git rebase (--continue | --skip | --abort | --quit | --edit-todo | --show-current-patch)

上述命令可以简化为如下格式:

# 将 topic_branch 变基到 base_branch 的最新提交,
# 当未指定 topic_branch 时,则默认变基当前分支
git rebase [base_branch] [topic_branch]

# 交互式变基
git rebase -i [branch]

# 变基(继续 | 跳过 | 停止 | 退出 | 继续编辑 | 显示当前差异包)
git rebase (--continue | --skip | --abort | --quit | --edit-todo | --show-current-patch)

下面我们主要对git rebase的两个主要功能进行讲解:

git cherypick

在多分支工作流中,当我们需要获取另一个分支的所有变动时,通常采用的都是分支合并(git merge)策略,但是如果我们只对分支的一个或某几个提交感兴趣,那么也可以只摘取这几个提交,将他们各自的修改一一应用到我们当前分支上,Git 中,具备提交摘取的命令为git cherry-pick,其具体语法如下所示:

# 支持摘取多个 commit
git cherry-pick [<options>] <commit-ish>...
git cherry-pick (--continue | --skip | --abort | --quit)

git cherry-pick的本质是摘取提交,将其修改应用到当前分支上。
git cherry-pick支持摘取一个或多个提交,每一个提交应用到当前分支,都会生成一个新的提交,该提价的修改完全与摘取的提交一致。其实git cherry-pick就是将摘取的提交在当前分支上进行重复播放。

git cherry-pick常用的命令选项有如下:

举个例子:假设本地仓库存在masterdev分支,现在突然发现线上版本出现漏洞,因此紧急从master分支上创建一个hotfix/add_file分支,然后做了两个提交,如下图所示:

cherrypick - initial

简单起见,每个提交都只是增加了相应数字的文件。

当漏洞修改完成后,就需要将hotfix/add_file分支合并到master分支中:

$ git switch master
Switched to branch 'master'

# 合并 hotfix 分支
$ git merge --no-ff hotfix/add_file -m 'fix: C6 => merge branch hotfix/add_file'
Merge made by the 'recursive' strategy.
 4.txt | 1 +
 5.txt | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 4.txt
 create mode 100644 5.txt

此时的示意图如下所示:

cherrypick - master merge hotfix

同样的,hotfix/add_file分支上的修改也要合并到dev分支上,此时,dev分支可以git cherry-pick或直接合并hotfix/add_file分支,也可以git cherry-pick主分支master上的合并提交C6,下面对这两种方法分别进行讲解:

git bisect

git bisect命令可以让我们很方便快速查找到出现 bug 的提交,它的原理是对给定范围的提交进行二分查找,由用户判断当前提交是否存在 bug,依次迭代不断缩小规模进行二分查找,这样我们就可以很快从一个大范围提交区间找到引入 bug 的那个提交。

git bisect命令的具体语法如下所示:

# 启动二分查找
git bisect start [<paths>...]
# 当前提交存在 bug
git bisect bad [<rev>]
# 当前提价良好(即不存在 bug)
git bisect good [<rev>...]
# 退出二分查找
git bisect reset [<commit>]
git bisect terms [--term-good | --term-bad]
git bisect skip [(<rev>|<range>)...]
git bisect (visualize|view)
git bisect replay <logfile>
git bisect log
git bisect run <cmd>...
git bisect help

可以看到,git bisect携带了很多的子命令选项,但是通常我们只会使用git bisect { start | good | bad | reset }这四个子命令来进行二分查找。其中:

举个例子:假设我们当前仓库存在 5 个提交历史,为了方便演示,我们假设某个提交删除了ReadMe.md文件,现在想找出删除该文件的提交,操作过程如下:

# 所有提交
$ git log --oneline
15c3cdb (HEAD -> master) feat: 555
7b93852 feat: 444
c40d88f feat: 333
b9ce941 docs: add ReadMe.md
58bdc1d feat: 111

# HEAD 58bdc1d 表示对所有提交进行二分查找,这里也可以忽略不写
$ git bisect start HEAD 58bdc1d
Bisecting: 1 revision left to test after this (roughly 1 step)
[c40d88fc907538e7392509c7b221ebf78ae42516] feat: 333             # 表示当前处于 c40d88f,也就第三个提交

# 查看当前提交,可以看到,确实是处于第三个提交
$ git show HEAD --oneline --stat -s
c40d88f (HEAD) feat: 333

# 当前提交存在 ReadMe.md
$ ls
1.txt  3.txt  ReadMe.md

# 由于当前提交存在 ReadMe.md,故设置为良好状态
$ git bisect good
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[7b9385209361db20ce6b1d4e1e7c81d999ae84b5] feat: 444             # 此时处于 7b93852,即第四个提交

# 当前提交不存在 ReadMe.md
$ ls
1.txt  3.txt  4.txt

# 由于当前提交不存在 ReadMe.md,故将其设置为 bad 状态
$ git bisect bad
7b9385209361db20ce6b1d4e1e7c81d999ae84b5 is the first bad commit # 这里表示当前提交就是第一个引入 bug 的提交
commit 7b9385209361db20ce6b1d4e1e7c81d999ae84b5
Author: Why8n <Why8n@gmail.com>
Date:   Sun Jan 10 18:18:08 2021 +0800

    feat: 444

 4.txt     | 1 +
 ReadMe.md | 1 -
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 4.txt
 delete mode 100644 ReadMe.md                                    # 删除了文件 ReadMe.md

# 找到引入 bug 的提交后,就可以退出二分查找了
$ git bisect reset
Previous HEAD position was 7b93852 feat: 444
Switched to branch 'master'

# 因为删除了 ReadMe.md 的提交还进行了其他修改,因此这里不能直接使用 git revert
# 但是既然找到了删除 ReadMe.md 的提交,那么我们只需从该提交之前的提交获取 ReadMe.md 文件,进行恢复即可
# 7b93852 提交删除了 ReadMe.md,该提交之前的提交为 c40d88f
$ git log --oneline | grep 7b93852 -A 1
7b93852 feat: 444
c40d88f feat: 333

# 查询 c40d88f 所有文件,获取 ReadMe.md 的对象文件
$ git ls-tree c40d88f
100644 blob 58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c    1.txt
100644 blob 55bd0ac4c42e46cd751eb7405e12a35e61425550    3.txt
100644 blob c200906efd24ec5e783bee7f23b5d7c941b0c12c    ReadMe.md # 目标文件

# 将目标文件内容写到当前工作目录
$ git cat-file -p c20090 > ReadMe.md

以上,就是git bisect的整个基本操作过程。

查看合并提交的parent-number

前面我们介绍过的git revertgit cherry-pick等命令,在遇到合并提交时,都需要指定主线分支,也即parent-number。Git 似乎并没有直接提供查询parent-number的命令,因此我们只能手动进行查找。

我们以一个例子来驱动讲解查找合并提交的parent-number,假设现在我们有一个合并提交02d311d,可以通过如下命令查看其内容:

# 法一
git show --pretty=raw <merge_commit>

# 法二
git cat-file -p <merge_commit>

比如,查看合并提交02d311d,结果如下:

$ git show --pretty=raw 02d311d
commit 02d311d5c9bb0989c1285e068fc3c2a4de02b027
tree 03c45eebbff1c1fa9f9152d9800d4e65f4602052
parent d0b972f09705aaf330c59be6eedbd69a1e49ccbc    # parent1_commit,其 parent-number = 1
parent 35a10e51533ae17c42ecdf3ad9598334cdaeca08    # parent2_commit,其 parent-number = 2
author Why8n <Why8n@gmail.com> 1610459729 +0800
committer Why8n <Why8n@gmail.com> 1610459729 +0800

    fix: C6 => merge branch hotfix/add_file

该命令会显示合并提交的父提交,第一个parent1_commitparent-number就是1,第二个parent2_commitparetn-number就是2,依次类推...

然后,我们只需一一比对parent_commit与合并提交之间的差别,就可以判断得出应当使用哪个parent_commit了:

git diff --stat <parent_commit> <merge_commit>

更多详细内容,请参考:Git cherry-pick syntax and merge branches

其他

git blame

如果我们想查看文件每一行对应的版本以及最后修改的作者时,则可以使用git blame命令。其具体语法如下所示:

git blame [<options>] [<rev-opts>] [<rev>] [--] <file>

下面列举几种常用的git blame操作:

git gc

前文说过,Git 本质是一个全量快照的文件系统,因此当我们暂存次数过多时,Git 对象数据库会存储很多对象文件,有些对象文件实际上没有被任何提交对象直接或间接进行引用,这些对象称为『松散对象(loose objects)』。

我们使用命令git gc来垃圾回收这些松散对象,减小仓库大小。简单来说,当运行git gc时,Git 会收集所有松散对象并将它们存入一个packfile文件中,并将多个packfile文件合并成一个大的packfile文件,然后移除不被任何提交引用且超过一定期限的对象文件。除此之外,git gc还会将所有的引用文件(即.git/refs)打包到另一个单独的文件中。

更多 Git 垃圾回收相关内容,可参考如下文章:

分页器

Git 中几乎所有命令都提供了分页器(Pager)功能,当命令输出超出一页时,会自动启动分页器。

分页器的交互方式并不人性化,可通过如下几种方法进行分页:

别名

可以通过git config alias来为其他命令设置一个简短的别名,方便使用,比如:

$ git config --global alias.br branch

$ git br # ==> 扩展为:git branch

上述命令为branch设置了一个别名br,此时使用git br就相当于使用git branch

:Git 的别名就是一个字符串,使用时会自动扩展为设置的内容,自动拼接到git指令后面。

Git 也可以为外部命令指定别名,只需在命令前面添加!即可:

$ git config --global alias.ls '!ls -alrt'

$ git ls # ==> ls -alrt

举个例子:这里我们设置一个别名(命令),来显示本地仓库对象数据库所有对象文件及其类型:

# 由于命令太长,我们选择直接在全局配置文件中进行修改,方便很多
$ git config --global --edit

然后在标签[alias]下设置如下内容:

[alias]
    ; 注释:sdo represent show data objects
    sdo = "!find .git/objects -type f | awk -F '/' '{ hash=$3$4; cmd = sprintf(\"git cat-file -t %s\", hash); printf(\"%s\t\", hash); system(cmd); }'"

此时使用命令git sdo就可以显示对象数据库中所有的对象文件及其类型:

# sdo represents show data objects
$ git sdo
0767f3e206a0a431633b2063bbda680026c33f70        commit
10f86d6b803b8962653f16a9967a4578215dcb22        tree
778d49177a4b6da0e967ac3e9308076ad500e6e7        blob

git clean

如果需要移除工作区中未被追踪的文件或文件夹,可以使用git clean命令,其语法如下所示:

git clean [-d] [-f] [-i] [-n] [-q] [-e <pattern>] [-x | -X] [--] [<paths>]...

其中,常用的选项有:

版本及范围表示法

大多数 Git 命令都会携带一个revision(修订版本)作为参数,因此 Git 也内置了一些版本及其范围的简便引用方法,大致有如下:

更多其他版本与版本范围表示法,可参考:Git 版本及版本范围表示法

附录

本人配置

以下是本人的 Git 配置选项:

# 用户名
git config --global user.name Why8n
# 邮箱
git config --global user.email whyncai@gmail.com
# 默认编辑器
git config --global core.editor nvim

# 解决 git status 中文乱码
git config --global core.quotepath false

# 设置 git gui 界面编码
git config --global gui.encoding utf-8

# 设置 git log 提交内容编码
git config --global i18n.commitencoding utf-8 

# 分页器替换
git config --global core.pager "less -FRSX"

参考

上一篇 下一篇

猜你喜欢

热点阅读