[collect]如何在Git里撤销(几乎)任何操作


原文地址:

https://github.com/blog/2019-how-to-undo-almost-anything-with-git

译文地址:

http://blog.jobbole.com/87700/

==

任何版本控制系统的一个最有的用特性就是“撤销 (undo)”你的错误操作的能力。在 Git 里,“撤销” 蕴含了不少略有差别的功能。

当你进行一次新的提交的时候,Git 会保存你代码库在那个特定时间点的快照;之后,你可以利用 Git 返回到你的项目的一个早期版本。

在本篇博文里,我会讲解某些你需要“撤销”已做出的修改的常见场景,以及利用 Git 进行这些操作的最佳方法。

撤销一个“已公开”的改变

场景: 你已经执行了 git push, 把你的修改发送到了GitHub,现在你意识到这些 commit 的其中一个是有问题的,你需要撤销那一个 commit.

方法: git revert <SHA>

原理: git revert 会产生一个新的 commit,它和指定 SHA 对应的 commit 是相反的(或者说是反转的)。如果原先的 commit 是“物质”,新的 commit 就是“反物质” — 任何从原先的 commit 里删除的内容会在新的 commit 里被加回去,任何在原先的 commit 里加入的内容会在新的 commit 里被删除。

这是 Git 最安全、最基本的撤销场景,因为它并不会改变历史 — 所以你现在可以  git push 新的“反转” commit 来抵消你错误提交的 commit。

修正最后一个 commit 消息

场景: 你在最后一条 commit 消息里有个笔误,已经执行了 git commit -m "Fxies bug #42",但在 git push 之前你意识到消息应该是 “Fixes bug #42″。

方法:

git commit --amend
或
git commit --amend -m "Fixes bug #42"

原理: git commit --amend 会用一个新的 commit 更新并替换最近的 commit ,这个新的 commit 会把任何修改内容和上一个 commit 的内容结合起来。如果当前没有提出任何修改,这个操作就只会把上次的 commit 消息重写一遍。

撤销“本地的”修改

场景: 一只猫从键盘上走过,无意中保存了修改,然后破坏了编辑器。不过,你还没有 commit 这些修改。你想要恢复被修改文件里的所有内容 — 就像上次 commit 的时候一模一样。

方法:

git checkout -- <bad filename>

原理: git checkout 会把工作目录里的文件修改到 Git 之前记录的某个状态。你可以提供一个你想返回的分支名或特定 SHA ,或者在缺省情况下,Git 会认为你希望 checkout 的是 HEAD,当前 checkout 分支的最后一次 commit。

记住:你用这种方法“撤销”的任何修改真的会完全消失。因为它们从来没有被提交过,所以之后 Git 也无法帮助我们恢复它们。你要确保自己了解你在这个操作里扔掉的东西是什么!(也许可以先利用 git diff 确认一下)

重置“本地的”修改

场景: 你在本地提交了一些东西(还没有 push),但是所有这些东西都很糟糕,你希望撤销前面的三次提交 — 就像它们从来没有发生过一样。

方法:

git reset <last good SHA>
或
git reset --hard <last good SHA>

原理: git reset 会把你的代码库历史返回到指定的 SHA 状态。 这样就像是这些提交从来没有发生过。缺省情况下, git reset 会保留工作目录。这样,提交是没有了,但是修改内容还在磁盘上。这是一种安全的选择,但通常我们会希望一步就“撤销”提交以及修改内容 — 这就是 --hard 选项的功能。

在撤销“本地修改”之后再恢复

场景: 你提交了几个 commit,然后用 git reset --hard 撤销了这些修改(见上一段),接着你又意识到:你希望还原这些修改!

方法:

git reflog 和 git reset 或 git checkout

原理: git reflog 对于恢复项目历史是一个超棒的资源。你可以恢复几乎任何东西 — 任何你 commit 过的东西 — 只要通过 reflog。

你可能已经熟悉了 git log 命令,它会显示 commit 的列表。 git reflog 也是类似的,不过它显示的是一个 HEAD 发生改变的时间列表.

一些注意事项

  • 它涉及的只是 HEAD 的改变。在你切换分支、用 git commit 进行提交、以及用 git reset 撤销 commit 时,HEAD 会改变,但当你用 git checkout -- <bad filename> 撤销时(正如我们在前面讲到的情况),HEAD 并不会改变 — 如前所述,这些修改从来没有被提交过,因此 reflog 也无法帮助我们恢复它们。
  • git reflog 不会永远保持。Git 会定期清理那些 “用不到的” 对象。不要指望几个月前的提交还一直躺在那里。
  • 你的 reflog 就是你的,只是你的。你不能用 git reflog 来恢复另一个开发者没有 push 过的 commit。

reflog

那么…你怎么利用 reflog 来“恢复”之前“撤销”的 commit 呢?它取决于你想做到的到底是什么:

  • 如果你希望准确地恢复项目的历史到某个时间点,用 git reset --hard <SHA>
  • 如果你希望重建工作目录里的一个或多个文件,让它们恢复到某个时间点的状态,用 git checkout <SHA> -- <filename>
  • 如果你希望把这些 commit 里的某一个重新提交到你的代码库里,用 git cherry-pick <SHA>

利用分支的另一种做法

场景: 你进行了一些提交,然后意识到你开始 check out 的是 master 分支。你希望这些提交进到另一个特性(feature)分支里。

方法:

git branch feature, git reset --hard origin/master, and git checkout feature

原理: 你可能习惯了用 git checkout -b <name> 创建新的分支 — 这是创建新分支并马上 check out 的流行捷径 — 但是你不希望马上切换分支。这里, git branch feature 创建一个叫做 feature 的新分支并指向你最近的 commit,但还是让你 check out 在 master 分支上。

下一步,在提交任何新的 commit 之前,用 git reset --hard 把 master 分支倒回 origin/master 。不过别担心,那些 commit 还在 feature 分支里。

最后,用 git checkout 切换到新的 feature 分支,并且让你最近所有的工作成果都完好无损。

及时分支,省去繁琐

场景: 你在 master 分支的基础上创建了 feature 分支,但 master 分支已经滞后于 origin/master 很多。现在 master 分支已经和 origin/master 同步,你希望在 feature 上的提交是从现在开始,而不是也从滞后很多的地方开始。

方法:

git checkout feature 和 git rebase master

原理: 要达到这个效果,你本来可以通过 git reset (不加 --hard, 这样可以在磁盘上保留修改) 和 git checkout -b <new branch name> 然后再重新提交修改,不过这样做的话,你就会失去提交历史。我们有更好的办法。

git rebase master 会做如下的事情:

  • 首先它会找到你当前 check out 的分支和 master 分支的共同祖先。
  • 然后它 reset 当前  check out 的分支到那个共同祖先,在一个临时保存区存放所有之前的提交。
  • 然后它把当前 check out 的分支提到 master 的末尾部分,并从临时保存区重新把存放的 commit 提交到 master 分支的最后一个 commit 之后。

大量的撤销/恢复

场景: 你向某个方向开始实现一个特性,但是半路你意识到另一个方案更好。你已经进行了十几次提交,但你现在只需要其中的一部分。你希望其他不需要的提交统统消失。

方法:

git rebase -i <earlier SHA>

原理: -i 参数让 rebase 进入“交互模式”。它开始类似于前面讨论的 rebase,但在重新进行任何提交之前,它会暂停下来并允许你详细地修改每个提交。

rebase -i 会打开你的缺省文本编辑器,里面列出候选的提交。如下所示:

rebase-interactive1

前面两列是键:第一个是选定的命令,对应第二列里的 SHA 确定的 commit。缺省情况下, rebase -i  假定每个 commit 都要通过  pick 命令被运用。

要丢弃一个 commit,只要在编辑器里删除那一行就行了。如果你不再需要项目里的那几个错误的提交,你可以删除上例中的1、3、4行。

如果你需要保留 commit 的内容,而是对 commit 消息进行编辑,你可以使用 reword 命令。 把第一列里的 pick 替换为 reword (或者直接用 r)。有人会觉得在这里直接重写 commit 消息就行了,但是这样不管用 —rebase -i 会忽略 SHA 列前面的任何东西。它后面的文本只是用来帮助我们记住 0835fe2 是干啥的。当你完成 rebase -i 的操作之后,你会被提示输入需要编写的任何 commit 消息。

如果你需要把两个 commit 合并到一起,你可以使用 squash 或 fixup 命令,如下所示:

rebase-interactive2

squash 和 fixup 会“向上”合并 — 带有这两个命令的 commit 会被合并到它的前一个 commit 里。在这个例子里, 0835fe2 和 6943e85 会被合并成一个 commit, 38f5e4e 和 af67f82 会被合并成另一个。

如果你选择了 squash, Git 会提示我们给新合并的 commit 一个新的 commit 消息; fixup 则会把合并清单里第一个 commit 的消息直接给新合并的 commit 。 这里,你知道 af67f82 是一个“完了完了….” 的 commit,所以你会留着 38f5e4e as的 commit 消息,但你会给合并了 0835fe2 和 6943e85 的新 commit 编写一个新的消息。

在你保存并退出编辑器的时候,Git 会按从顶部到底部的顺序运用你的 commit。你可以通过在保存前修改 commit 顺序来改变运用的顺序。如果你愿意,你也可以通过如下安排把 af67f82 和 0835fe2 合并到一起:

rebase-interactive3

修复更早期的 commit

场景: 你在一个更早期的 commit 里忘记了加入一个文件,如果更早的 commit 能包含这个忘记的文件就太棒了。你还没有 push,但这个 commit 不是最近的,所以你没法用 commit --amend.

方法:

git commit --squash <SHA of the earlier commit>
和
git rebase --autosquash -i <even earlier SHA>

原理: git commit --squash 会创建一个新的 commit ,它带有一个 commit 消息,类似于 squash! Earlier commit。 (你也可以手工创建一个带有类似 commit 消息的 commit,但是 commit --squash 可以帮你省下输入的工作。)

如果你不想被提示为新合并的 commit 输入一条新的 commit 消息,你也可以利用 git commit --fixup 。在这个情况下,你很可能会用commit --fixup ,因为你只是希望在 rebase 的时候使用早期 commit 的 commit 消息。

rebase --autosquash -i  会激活一个交互式的 rebase 编辑器,但是编辑器打开的时候,在 commit 清单里任何 squash! 和 fixup! 的 commit 都已经配对到目标 commit 上了,如下所示:

rebase-autosquash

在使用 --squash 和 --fixup 的时候,你可能不记得想要修正的 commit 的 SHA 了— 只记得它是前面第 1 个或第 5 个 commit。你会发现 Git 的 ^ 和 ~ 操作符特别好用。HEAD^ 是 HEAD 的前一个 commit。 HEAD~4 是 HEAD 往前第 4 个 – 或者一起算,倒数第 5 个 commit。

停止追踪一个文件

场景: 你偶然把 application.log 加到代码库里了,现在每次你运行应用,Git 都会报告在 application.log 里有未提交的修改。你把 *.login 放到了 .gitignore 文件里,可文件还是在代码库里 — 你怎么才能告诉 Git “撤销” 对这个文件的追踪呢?

方法:

git rm --cached application.log

原理: 虽然 .gitignore 会阻止 Git 追踪文件的修改,甚至不关注文件是否存在,但这只是针对那些以前从来没有追踪过的文件。一旦有个文件被加入并提交了,Git 就会持续关注该文件的改变。类似地,如果你利用 git add -f 来强制或覆盖了 .gitignore, Git 还会持续追踪改变的情况。之后你就不必用 -f 来添加这个文件了。

如果你希望从 Git 的追踪对象中删除那个本应忽略的文件,git rm --cached 会从追踪对象中删除它,但让文件在磁盘上保持原封不动。因为现在它已经被忽略了,你在  git status 里就不会再看见这个文件,也不会再偶然提交该文件的修改了。


这就是如何在 Git 里撤销任何操作的方法。要了解更多关于本文中用到的 Git 命令,请查看下面的有关文档:

=EOF=


《 “[collect]如何在Git里撤销(几乎)任何操作” 》 有 22 条评论

  1. 如何删除Git仓库中未被追踪的文件「git remove Untracked files」
    `$ git clean -n
    $ git clean -fdx
    `
    http://stackoverflow.com/questions/8200622/how-to-remove-untracked-files-in-git
    http://stackoverflow.com/questions/61212/how-to-remove-local-untracked-files-from-the-current-git-branch

    如何删除Git仓库中已被添加的文件「undo ‘git add’ before commit」
    `$ git reset HEAD (file) #或
    $ git rm –cached (added_file_to_undo)
    `
    http://stackoverflow.com/questions/348170/how-to-undo-git-add-before-commit
    http://stackoverflow.com/questions/1505948/how-do-i-remove-a-single-file-from-the-staging-area-of-git-but-not-remove-it-fro

  2. 如何在本地新建一个分支用于跟踪远程的分支?
    `
    # 第一步
    $ git fetch
    # 或
    $ git pull

    # 第二步
    $ git checkout -b newLocalBranch remoteName/remoteBranch
    # 或
    $ git branch –track newLocalBranch remoteName/remoteBranch
    `
    Track a new remote branch created on GitHub
    https://stackoverflow.com/questions/11262703/track-a-new-remote-branch-created-on-github
    Make an existing Git branch track a remote branch?
    https://stackoverflow.com/questions/520650/make-an-existing-git-branch-track-a-remote-branch

  3. Git 手欠恢复技巧
    https://www.40huo.cn/blog/git-trouble.html
    https://services.github.com/on-demand/git-trouble/
    `
    搭建练习环境(Set up your Git Scenario Environment)
    创建了太多细碎的 Commit(Too Many (small) Git Commits)
    翔一样的 Commit Message(Git Commit Message Sucks)
    提交到了错误的分支(Git Committed to Wrong Branch)
    意外 commit(Accidental Git Commit)
    推倒重来(Just Make it Go Away – Breaking Things With Git)
    `

  4. git中如何查看某个文件的修改历史
    https://stackoverflow.com/questions/278192/view-the-change-history-of-a-file-using-git-versioning
    https://stackoverflow.com/questions/9807393/git-show-history-of-a-file
    https://stackoverflow.com/questions/3701404/list-all-commits-for-a-specific-file
    http://www.cnblogs.com/flyme/archive/2011/11/28/2265899.html
    `
    $ git log path/to/file #查看和file相关的commit记录
    $ git log -p path/to/file #显示每次提交的diff
    # or
    $ gitk path/to/file
    $ gitk –follow path/to/file
    `

  5. 如何将代码发布至新仓库?
    `
    #先clone到本地,然后添加文件再进行add+commit+push
    git clone https://github.com/my_username/my_repo
    … #一些编辑操作
    git add –all
    git commit -m ‘Initial Commit’
    git remote add origin https://github.com/my_username/my_repo
    git push origin master

    #如果你本地已经有准备好要上传至仓库的代码了,可以执行下面的命令:
    cd existing-project
    git init
    git add –all
    git commit -m ‘Initial Commit’
    git remote add origin https://github.com/my_username/my_repo
    git push origin master

    #如果你的代码已经使用Git进行管理了(之前就是一个Git仓库),那么可以使用下面的命令将老仓库设置为新仓库的”origin”
    cd existing-project
    git remote set-url origin https://github.com/my_username/my_repo
    git push origin master
    `

  6. 如何克服解决Git冲突的恐惧症?
    https://juejin.im/post/5aa28c9c518825556140de45
    `
    关键方法->减少冲突:
    要减少冲突(即将它扼杀在萌芽阶段,/捂脸),关键在于组织和分工、沟通和习惯,如果架构设计耦合太多、分工有交叉、沟通少,那么无论用什么版本控制工具都不能解决冲突的问题。

    治标方法->立竿见影:
    “熟练掌握reflog+reset”告诉你后悔药是如何吃的,这就跟学车是一样的道理,一定要先学会踩刹车,心里才有底。不过,初次接触 reflog 的界面的童鞋很可能会不太熟悉,不知道怎么退出去,不要害怕,谷歌会给你答案。

    治本方法->壮胆神药:
    掌握Git基础和原理。

    辅助方法->开门见山:
    装个好的前端比如SourceTree,一般性的操作,都可以不用指令,出了错有UI界面,解决冲突时只需动动鼠标,轻松加写意。
    `

  7. git删除所有历史提交记录
    https://blog.csdn.net/tp7309/article/details/79173587
    https://yuhongjun.github.io/tech/2017/04/28/git%E4%BB%93%E5%BA%93%E5%88%A0%E9%99%A4%E6%89%80%E6%9C%89%E6%8F%90%E4%BA%A4%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95-%E6%88%90%E4%B8%BA%E4%B8%80%E4%B8%AA%E5%B9%B2%E5%87%80%E7%9A%84%E6%96%B0%E4%BB%93%E5%BA%93.html
    https://stackoverflow.com/questions/13716658/how-to-delete-all-commit-history-in-github
    `
    把旧项目提交到Git上,但是会有一些历史记录,这些历史记录中可能会有项目密码等敏感信息。如何删除这些历史记录,形成一个全新的仓库,并且保持代码不变呢?

    1.Checkout
    git checkout –orphan latest_branch

    2. Add all the files
    git add -A

    3. Commit the changes
    git commit -am “commit message”

    4. Delete the branch
    git branch -D master

    5.Rename the current branch to master
    git branch -m master

    6.Finally, force update your repository
    git push -f origin master
    `

  8. git查看某个文件的提交记录
    https://blog.csdn.net/lxf0613050210/article/details/54598941
    `
    git blame filename //查看每一行是哪次提交最后修改的

    git log filename //简单列出和某个文件相关的提交
    git log -p filename //列出文件的所有修改记录(可以显示每次提交的diff)

    git show $commit_id filename //只看某次提交中的某个文件变化,可以直接加上filename
    `

    git查看某个文件的修改历史
    https://www.cnblogs.com/Caden-liu8888/p/8252445.html
    http://www.cnblogs.com/flyme/archive/2011/11/28/2265899.html

    git查看某个文件修改历史
    https://www.cnblogs.com/tigerson/p/7149947.html

  9. git忽略特殊文件
    https://blog.ipsfan.com/5020.html
    `
    1)以“/”开头表示目录;
    2)以“?”通配单个字符
    3)以“*”通配多个字符;
    4)以方括号“[]”包含单个字符的匹配列表;
    5)以叹号“!”跟踪某个文件或目录;

    *.log
    wp-config.php
    wp-content/advanced-cache.php
    wp-content/backup-db/
    wp-content/backups/
    wp-content/blogs.dir/
    wp-content/cache/
    wp-content/upgrade/
    wp-content/uploads/
    wp-content/mu-plugins/
    wp-content/wp-cache-config.php
    wp-content/plugins/hello.php

    /.htaccess
    /license.txt
    /readme.html
    /sitemap.xml
    /sitemap.xml.gz
    `

  10. 清除github所有历史提交记录的方法
    https://ops-coffee.cn/s/github-remove-commit.html
    `
    1、进入本地存放代码的目录删除 .git 隐藏目录
    2、windows+R输入cmd进入windows命令行
    3、切换到操作目录
    4、执行 git init 命令初始化本地目录为一个git仓库
    5、执行 git add -A 命令添加本地代码到仓库
    6、执行 git commit 命令提交本地代码到仓库
    7、执行 git push -f 命令强制提交到远程仓库
    `

  11. git stash暂存修改
    https://blog.csdn.net/Jason_Lewis/article/details/80091942
    https://blog.csdn.net/dawn_moon/article/details/6977388
    `
    有时候你正在写代码,写到一半的时候需要切到另一个分支修改一个bug,这时直接切换分支是不行的,会提示让你先提交修改或者暂存起来。代码写到一半显然不能直接提交到仓库,这时候就需要用到git stash命令了,命令如下:

    git stash // 把当前进度保存到暂存区

    再输入命令 git status 就会告诉你nothing to commit,这时我们就可以正常切换分支了,在另一个分支修改完成之后,再切回到当前分支使用下面的命令:

    git stash pop // 恢复最新的进度到工作区

    然后我们就可以接着愉快的写代码了。

    这只是简单的暂存与恢复命令,还可以参考以下命令,结合使用:

    git stash save ‘message’ // 同git stash只不过增加了一些注释信息
    git stash list // 显示暂存列表
    git stash pop [stash] // 默认弹出最上面的那一条即stash@{0},还可以指定弹出那一条及pop后加stash@{n}
    git stash drap [stash] // 默认丢弃最上面的那一条即stash@{0},可以指定弹出哪一条同上
    git stash clear // 清除所有的stash
    git show stash@{n} // 通过此命令可以查看stash的详情
    git apply stash@{n} // 与pop类似,只不过不会丢弃stash

    其他更多关于stash的命令请使用 git stash –help 命令查看Git manual。
    `

  12. 看完这一篇,再也不用担心 Git 的“黑魔法”
    https://mp.weixin.qq.com/s/wWgapBH1YRxLuf2CuZhKBw
    `
    本文分享 Git 使用上的一些基础知识,通俗易懂,非常有用。

    在 Git Rev News #48 期的 LightReading 中有一篇文章(地址:https://hacker-tools.github.io/version-control/)写的不错,不仅干货满满而且还附带了操作视频。其中的内容不仅覆盖了很多 Git 使用上的基础知识,也从使用角度上解答了很多刚接触 Git 的开发者的疑问。为了便于读者理解,我在翻译的同时也添加了一些内容。以下为正文部分。本文内容较长,建议收藏慢慢学习。
    `
    https://hacker-tools.github.io/version-control/

  13. git中如何比较不同commit之间的变动和区别(git diff between commits)
    https://git-scm.com/docs/git-diff
    https://docs.github.com/en/github/committing-changes-to-your-project/comparing-commits
    `
    Comparing branches
    Comparing tags
    Comparing commits
    Comparing across forks
    Comparisons across commits
    `
    https://stackoverflow.com/questions/3368590/show-diff-between-commits
    `
    git diff oldCommit..newCommit

    # 以下2个命令等价
    git diff HEAD^^ HEAD
    git diff HEAD~2
    `

  14. 【译】10 个最有用的 git log 技巧
    https://mp.weixin.qq.com/s/ywSOLf_YPK37ry69sm40ew
    https://hackernoon.com/ten-useful-git-log-tricks-7nt3yxy
    `
    git log –oneline
    git log –after=”2020-05-15″
    git log –after=”2020-05-15″ –before=”2020-05-25″
    git log -p
    git log –author=”Srebalaji”
    git log –after=”1 week ago” –author=”srebalji” -p
    git log -i –grep=”issue-43560\|issue-89786″
    git log main.rb
    git log -S”function login()”
    git log –merges
    git log master..develop
    git log –pretty=format:”%Cred%an – %ar%n %Cblue %h -%Cgreen %s %n”
    `

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注