/

【Git 权威指南】读书笔记 - 独奏 - Part 2

主要内容:【Git 重置】、【Git 检出】、【恢复进度】

Git 重置

分支游标 master 的探秘

git log --graph --oneline
* e695606 which version checked in?
* a0c641e who does commit?
* 9e8a761 initialized.

引用 refs/heads/master 就好像是一个游标,在有新的提交发生的时候指向了新的提交。

Git 提供了 git reset 命令,可以将“游标”指向任意一个存在的提交 ID。注意下面的命令中使用了 --hard 参数,会破坏工作区未提交的改动,慎用。

git reset --hard HEAD^
HEAD is now at e695606 which version checked in?

用 reflog 挽救错误的重置

通过 .git/logs 目录下日志文件记录了分支的变更。默认非裸版本库(带有工作区)都提供分支日志功能,这是因为带有工作区的版本库都有如下设置:

git config core.logallrefupdates
true

查看一下 master 分支的日志文件 .git/logs/refs/heads/master 中的内容。

tail -5 .git/logs/refs/heads/master

Git 提供了一个 git reflog 命令,对这个文件进行操作。

git reflog show master | head -5
9e8a761 master@{0}: 9e8a761: updating HEAD
e695606 master@{1}: HEAD^: updating HEAD
4902dc3 master@{2}: commit: does master follow this new commit?
...

重置 master 为两次改变之前的值。

git reset --hard master@{2}

深入了解 git reset 命令

git reset [-q] [<commit>] [--] <paths>...
git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]

为了避免路径和引用(或者提交 ID)同名而冲突,可以在 <paths> 前用两个连续的短线(减号)作为分隔。

20-got-git-reading-notes-solo-git-reset

  • --hard 会执行上图中的 1、2、3 全部的三个动作。
  1. 替换引用的指向。引用指向新的提交 ID。
  2. 替换暂存区。替换后,暂存区的内容和引用指向的目录树一致。
  3. 替换工作区。替换后,工作区的内容变得和暂存区一致,也和 HEAD 所指向的目录树内容相同。
  • --soft 会执行上图中的操作 1。
  • --mixed(缺省)会执行上图中的操作 1 和操作 2。
git reset
git reset HEAD
# 仅用 HEAD 指向的目录树重置暂存区,工作区不会受到影响,相当于将之前用 git add 命令更新到暂存区的内容撤出暂存区。引用也未改变,因为引用重置到 HEAD 相当于没有重置。

git reset -- filename
git reset HEAD filename
# 仅将文件 filename 撤出暂存区,暂存区中其他文件不改变。相当于对命令 git add filename 的反向操作。

git reset --soft HEAD^
# 工作区和暂存区不改变,但是引用向前回退一次。当对最新提交的提交说明或者提交的更改不满意时,撤销最新的提交以便重新提交。git commit 的反向操作。
# 在之前曾经介绍过一个修补提交命令 git commit --amend,用于对最新的提交进行重新提交以修补错误的提交说明或者错误的提交文件。修补提交命令实际上相当于执行了下面两条命令。(注:文件 .git/COMMIT_EDITMSG 保存了上次的提交日志)
git reset --soft HEAD^
git commit -e -F .git/COMMIT_EDITMSG

git reset HEAD^
# 工作区不改变,但是暂存区会回退到上一次提交之前,引用也会回退一次。

git reset --hard HEAD^
# 彻底撤销最近的提交。引用回退到前一次,而且工作区和暂存区都会回退到上一次提交的状态。自上一次以来的提交全部丢失。

Git 检出

重置命令 git reset 的一个用途就是修改引用(如 master)的游标。如果 HEAD 要改变该如何改变呢?检出命令 git checkout 该命令的实质就是修改 HEAD 本身的指向,该命令不会影响分支“游标”(如 master)。

HEAD 的重置即检出

git co HEAD^
Note: checking out 'HEAD^'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
# 您现在处于 '分离头指针' 状态。您可以检查、测试和提交,而不影响任何分支。
# 通过执行另外的一个 checkout 检出指令会丢弃在此状态下的修改和提交。

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
# 如果想保留在此状态下的修改和提交,使用 -b 参数调用 checkout 检出指令以
# 创建新的跟踪分支。如:

git checkout -b <new-branch-name>

HEAD is now at 3175afd...

什么叫做 detached HEAD “分离头指针”状态?查看一下此时 HEAD 的内容就明白了。

cat .git/HEAD
3175afde9450a1dc40b09d05a012b45e967cb80f

原来“分离头指针”状态指的就是 HEAD 头指针指向了一个具体的提交 ID,而不是一个引用(分支)。注意上面的 reflogHEAD 头指针的变迁记录,而非 master 分支。

查看一下 HEAD 和 master 对应的提交 ID,会发现现在它们指向的不一样。

git rev-parse HEAD master
3175afde9450a1dc40b09d05a012b45e967cb80f
bd08cb462d38b54b930cf1934b0c33f2e4592390

在“分离头指针”模式仍然可以进行提交:

git status
HEAD detached at 3175afd
...

但是在 checkout 到其他分支时,刚才的提交会丢失,但是这个提交仍然在版本库中,由于这个提交没有被任何分支跟踪到,因此并不能保证这个提交会永久存在。

实际上当 reflog 中含有该提交的日志过期后,这个提交随时都会从版本库中彻底清除。

挽救分离头指针

在“分离头指针”模式下进行的测试提交除了使用提交 ID acc2f69 访问之外,不能通过 master 分支或其他引用访问到。使用合并操作 git merge 将提交 acc2f69 合并到 master 分支中来。

git merge acc2f69
Merge made by recursive.
...

恢复进度

继续暂存区未完成的实践

# 保存当前工作进度
git stash

# 查看保存的进度用命令
git stash list

# 最近保存的进度进行恢复
git stash pop
git stash pop [--index] [<stash>]
# --index 除了恢复工作区的文件外,还尝试恢复暂存区
# 从该 <stash> 中恢复
git stash [save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
[-u|--include-untracked] [-a|--all] [<message>]]
  • --patch 会显示工作区和 HEAD 的差异,通过对差异文件的编辑决定在进度中最终要保存的工作区的内容,通过编辑差异文件可以在进度中排除无关内容。
  • 使用 -k 或者 --keep-index 参数,在保存进度后不会将暂存区重置。
# 不删除恢复的进度之外,其余和 git stash pop 命令一样
git stash apply [--index] [<stash>]

# 删除一个存储的进度
git stash drop [<stash>]

# 删除所有存储的进度
git stash clear

# 基于进度创建分支
git stash branch <branchname> <stash>

探秘 git stash

在执行 git stash 命令时,Git 实际调用了一个脚本文件实现相关的功能。

git --exec-path
/usr/lib/git-core

file /usr/lib/git-core/git-stash
/usr/lib/git-core/git-stash: POSIX shell script text executable

本地没有被版本控制系统跟踪的文件并不能保存进度。因此本地新文件需要执行添加 add 再执行 git stash 命令。

在用 git stash 命令保存进度时,提供说明更容易找到对应的进度文件。

每个进度的标识都是 stash@{<n>} 格式,像极了前面介绍的 reflog 的格式。git stash 的就是用到了前面介绍的引用和引用变更日志 reflog 来实现的。

用 git stash 保存进度,实际上会将进度保存在引用 refs/stash 所指向的提交中。多次的进度保存,实际上相当于引用 refs/stash 一次又一次的变化,而 refs/stash 引用的变化由 reflog(即.git/logs/refs/stash)所记录下来。

如何在引用 refs/stash 中同时保存暂存区的进度和工作区中的进度

git log --graph --pretty=raw  refs/stash -2
* commit e5c0cdc2dedc3e50e6b72a683d928e19a1d9de48
|\ tree 780c22449b7ff67e2820e09a6332c360ddc80578
| | parent 2b31c199d5b81099d2ecd91619027ab63e8974ef
| | parent c5edbdcc90addb06577ff60f644acd1542369194
| | author Jiang Xin <jiangxin@ossxp.com> 1291623066 +0800
| | committer Jiang Xin <jiangxin@ossxp.com> 1291623066 +0800
| |
| | WIP on master: 2b31c19 Merge commit 'acc2f69'
| |
| * commit c5edbdcc90addb06577ff60f644acd1542369194
|/ tree 780c22449b7ff67e2820e09a6332c360ddc80578
| parent 2b31c199d5b81099d2ecd91619027ab63e8974ef
| author Jiang Xin <jiangxin@ossxp.com> 1291623066 +0800
| committer Jiang Xin <jiangxin@ossxp.com> 1291623066 +0800
|
| index on master: 2b31c19 Merge commit 'acc2f69'

最新的提交说明中有 WIP(Work In Progess)字样,说明代表了工作区进度。而最新提交的第二个父提交(上图中显示为第二个提交)有 index on master 字样,说明这个提交代表着暂存区的进度。

References