Git 与其他版本控制系统的主要区别之一,在于其允许用户重写历史。实现这一目的的主要途径则是 git reabse
,通常还跟随着一句 git push --force
以用本地历史重写远端历史。
这里要谈论的是如何用rebase
、reset
和 commit
来分割既有的提交。
比方说在一次 commit 中,包含了两个编辑过的文件(A 和 B);但你只想把其中的 A 引入当前分支,B 则不需要。
使用 git cherry-pick
并不可行,因为它会把 A 和 B 的改变都拉过来。
解决方法是将那次 commit 分成两个,然后只 cherry-pick
包含了 A 的那个。
做法如下:
git rebase -i ~
(注意 ~
),或者 git rebase -i
pick
改成 edit
git reset HEAD~
以重置阶段性的改变git add [files-to-add]
所有本次需要用到的文件 (此处就是 A)git commit -m
git commit
git rebase --continue
以指示分割过程完成并退出变基操作最后,就可以用 git cherry-pick
将所需的新提交引入我们的分支中了。
# 1. git rebase 指定想要更改 commit 的前一个 commit,可以理解为 指定父节点,然后开始改动
git rebase -i <commit-hash>~ #(注意 `~`),
git rebase -i <hash-of-previous-commit>
# 2. 在编辑窗口中找到要更改的那次 commit,将其前面的 `pick` 改成 `edit`,退出后,git 会自动进行重演,停止到改为 edit 的 commit 处
# 3. 丢弃当前 commit,将此次 commit 变更文件放到工作区。
# 注意!执行该命令前,最好确认工作区和暂存区为空,否则会有些冲突
git reset HEAD~
# 4. 之后就和正常提交一样,执行 git add,git commit
# 5. 最后重演之后所有的 commit
git rebase --continue
# 原理简单讲解
# 1. git rebase -i commit-id
# 就是对 commit-id 后面的所有 commit 进行重演,就是相当于你自己执行 git add、git commit 逐个提交 commit
# - pick 标志: 就是对 commit 进行不做改动的重演,直到遇到非 pick 标志的 commit 才会停止
# 注意若删除了某条 commit,该 commit 就不会重演了
# - edit 标志: 就是指定重演时,停止的位置,可以理解为该 edit 标志 commit 提交后停了下来,等待你的编辑;
# 编辑完成后,执行 git rebase --continue 继续重演后续的 commit
# - squash 标志: 可以将多个历史 commit 进行合并。未使用过,理解为就是将多个相邻的 squash 标志的commit 合并为一个 commit
# 2. git reset HEAD~
# reset 就是重置 HEAD 指针的指向
# HEAD 可以理解为指针,指向最新的 commit
# HEAD~ 就是 HEAD 前1次的commit,即次新的 commit, HEAD~2 就是 HEAD 的前2次 commit
# 所以 git reset HEAD~ 就相当于,将 HEAD 指针向前移动 1 次,那么就会丢失最新的 commit
# 同理 git reset HEAD~2 就相当于,将 HEAD 指针向前移动 2 次,那么就会丢失最新的 2 个 commit
# 那么丢失的 commit 中的文件改动会哪去呢? —— 这就对应了 reset 的三种模式
# --mixed 模式: 也就是默认模式,会将所有丢失 commit 的文件改动等放到【工作区】
# --soft 模式: 也就是默认模式,会将所有丢失 commit 的文件改动等放到【暂存区】
# --hard 模式: 也就是默认模式,会将所有丢失 commit 的文件改动【丢弃】
# 所以,再执行 reset 前,最好将工作区和暂存区清空,否则可能有未注意到的合并,从而产生bug
# 另外执行 hard 模式要慎重,会造成文件的丢失
# 若不慎造成 commit 丢失,可以通过 reflog 进行恢复,但是无法恢复【工作区】和【暂存区】的文件丢失
| +---------------+
+------------>| commit-D | HEAD
| +---------------+
| +---------------+
+------------>| commit-C | HEAD^ 或 HEAD~
| +---------------+
| +---------------+
+------------>| commit-B | HEAD^^ 或 HEAD~2
| +---------------+
| +---------------+
+------------>| commit-A | HEAD~3
| +---------------+
|
v
Git - 重写历史
该命令主要用于修改历史 commit,如历史 commit 的拆分,合并等
注意使用方式
git rebase -i commit-B
或 git rebase -i HEAD~2(或HEAD^^)
执行 git rebase -i commit-B
后,会列出 commit-B 之后的所有 commit,之后 vim 形式操作
git reset commit-B
含义就是将 commit-B 之后的所有 commit 的更改恢复到工作区
git reset HEAD~2(或HEAD^^)
git reset | ||
---|---|---|
git reset --soft | 保留工作区,并把重置HEAD所带来的新的差异放进暂存区 | 注意可能会影响当前工作取的更改可能会覆盖历史的commit 的改动 (若当前工作区和历史 commit 对相同文件有改动) |
git reset --mixed (等同于 git reset) | 保留工作区,并清空暂存区 | 注意可能会影响当前工作区的更改可能会覆盖历史的commit 的改动 (若当前工作区和历史 commit 对相同文件有改动) |
git reset --hard | 重置工作区和暂存区 | 注意当前工作区的改动可能会丢失 (若当前工作区和历史 commit 对相同文件有改动) |
总结 | 最好在执行 reset 之前,清理干净工作区和暂存区,防止产生一些意外的变动无法评估 默认模式(mixed)会将历史 commit 的改动放回到工作区 hard 模式会丢弃 |
# 最好在执行 reset 之前,清理干净工作区和暂存区,防止一些意外的变动导致 bug
# hard 模式 慎用,可能会产生文件变动丢失
# 改动失误,造成丢失时,可尝试用 git reflog 查看历史记录,然后采用 git reset 进行恢复
如何指定回滚到哪里
HEAD 表示当前版本
HEAD^ 上一个版本
HEAD^^ 上上一个版本
HEAD~100 上100个版本,通用的
版本号 指定版本
可以配合不同的模式(–mixed, --soft, --hard)达到不同的效果。
首先查看commit日志
$ git log --pretty=oneline
1341074157aeb92de8caf982507d3f6c9280d5eb (HEAD -> master) commit 03
86b5d83fe42a1a9da8e96612a6c2c91f8e3e2001 commit 02
23dddd7b12606de10ed759cb72793d072ef2e48a commit 01
faa4214bc342ade5693a7efc8a64e869965c039e fix conflict
818c5faf28d0a0e5c8133dbd77dd24e6e70db9bf aaaaaaa
6f43203cf463dc5320916f96abef0f1ad63428fd (b1) xx
adda355046920ae91118cf42ec2f45190b0ec89c test
2e1b4bced0f0ce2c20362789be2878b36c6910f7 add t4
8262ea4e39ea80dc56056a667e9dbdcd235efc08 add t3
f2b85bf7f7516a6a6a0768e44266d09414b03a2e 2
01d308a7ef190b881969ea9b9112424819ab346a first commit
commit 01, commit 02, commit 03 为最近的三次提交,是提交时的备注信息。
HEAD -> master
表示当前HEAD处于 master分支的1341074157aeb92de8caf982507d3f6c9280d5eb
我们回到上一个版本 commit 02 去,也就是 86b5d83fe42a1a9da8e96612a6c2c91f8e3e2001
$ git reset --hard HEAD^
HEAD is now at 86b5d83 commit 02
或者 git reset --hard HEAD~1 或者 git reset --hard 86b5d83fe42a1a9da8e96612a6c2c91f8e3e2001
发现代码发生了变化,和预期一直。
再使用 git log 查看一下
$ git log --pretty=oneline
86b5d83fe42a1a9da8e96612a6c2c91f8e3e2001 (HEAD -> master) commit 02
23dddd7b12606de10ed759cb72793d072ef2e48a commit 01
faa4214bc342ade5693a7efc8a64e869965c039e fix conflict
818c5faf28d0a0e5c8133dbd77dd24e6e70db9bf aaaaaaa
6f43203cf463dc5320916f96abef0f1ad63428fd (b1) xx
adda355046920ae91118cf42ec2f45190b0ec89c test
2e1b4bced0f0ce2c20362789be2878b36c6910f7 add t4
8262ea4e39ea80dc56056a667e9dbdcd235efc08 add t3
f2b85bf7f7516a6a6a0768e44266d09414b03a2e 2
01d308a7ef190b881969ea9b9112424819ab346a first commit
发现 commit 03 已经看不到了,为什么呢?因为每一个commit只会保存它的parent节点,并不知道它的下一个节点时什么。那么问题来了,我又想回到 commit 03 该怎么办呢?
Git提供了一个命令git reflog
用来记录你的每一次命令
$ git reflog
86b5d83 (HEAD -> master) HEAD@{0}: reset: moving to HEAD^
1341074 HEAD@{1}: reset: moving to 1341074157aeb92de8caf982507d3f6c9280d5eb
86b5d83 (HEAD -> master) HEAD@{2}: reset: moving to HEAD
86b5d83 (HEAD -> master) HEAD@{3}: reset: moving to HEAD^
1341074 HEAD@{4}: commit: commit 03
86b5d83 (HEAD -> master) HEAD@{5}: commit: commit 02
23dddd7 HEAD@{6}: commit: commit 01
找到 commit 03 这一条,1341074 就是第一个版本号。
$ git reset --hard 1341074
HEAD is now at 1341074 commit 03
哈哈哈,我又回来了。