概括
首先修复本地历史记录。您有多种选择,这些选择的易用性各不相同,具体取决于您的历史记录的复杂程度 HEAD
以及意外删除的提交。
-
git reset --soft
-
git rebase --interactive
-
git commit-tree
-
git filter-repo
-
git filter-branch
(尽量避免这种情况)
如果你使用 rip 推送了历史记录,则可能需要修复共享存储库上的历史记录(删除并重新推送分支或 git push --force
),并且你的合作者将不得不 根据重写的历史记录重新调整他们的工作 .
您可能还会发现 “从存储库中删除敏感数据” 是一个有用的资源。
设置
我将使用具体的示例历史来说明可能的修复方法,该示例历史模拟了一个简单的代表性序列
-
添加
index.html
-
添加
site.css
和 oops.iso
-
添加
site.js
和删除 oops.iso
要在您的设置中重新创建此示例中的精确 SHA-1 哈希值,请首先设置几个环境变量。如果您使用的是 bash
export GIT_AUTHOR_DATE="Mon Oct 29 10:15:31 2018 +0900"
export GIT_COMMITTER_DATE="${GIT_AUTHOR_DATE}"
如果你在 Windows 命令 shell 中运行
set GIT_AUTHOR_DATE=Mon Oct 29 10:15:31 2018 +0900
set GIT_COMMITTER_DATE=%GIT_AUTHOR_DATE%
然后运行下面的代码。要在实验后回到相同的起点,请删除存储库,然后重新运行代码。
#! /usr/bin/env perl
use strict;
use warnings;
use Fcntl;
sub touch { sysopen FH, $_, O_WRONLY|O_CREAT and close FH or die "$0: touch $_: $!" for @_; 1 }
my $repo = 'website-project';
mkdir $repo or die "$0: mkdir: $!";
chdir $repo or die "$0: chdir: $!";
system(q/git init --initial-branch=main --quiet/) == 0 or die "git init failed";
system(q/git config user.name 'Git User'/) == 0 or die "user.name failed";
system(q/git config user.email '[email protected]'/) == 0 or die "user.email failed";
# for browsing history - http://blog.kfish.org/2010/04/git-lola.html
system "git config alias.lol 'log --graph --decorate --pretty=oneline --abbrev-commit'";
system "git config alias.lola 'log --graph --decorate --pretty=oneline --abbrev-commit --all'";
my($index,$oops,$css,$js) = qw/ index.html oops.iso site.css site.js /;
touch $index or die "touch: $!";
system("git add .") == 0 or die "A: add failed\n";
system("git commit -m A") == 0 or die "A: commit failed\n";
touch $oops, $css or die "touch: $!";
system("git add .") == 0 or die "B: add failed\n";
system("git commit -m B") == 0 or die "B: commit failed\n";
unlink $oops or die "C: unlink: $!"; touch $js or die "C: touch: $!";
system("git add .") == 0 or die "C: add failed\n";
system("git commit -a -m C") == 0 or die "C: commit failed\n";
system("git lol --name-status --no-renames");
输出显示存储库的结构是
* 1982cb8 (HEAD -> main) C
| D oops.iso
| A site.js
* 6e90708 B
| A oops.iso
| A site.css
* d29f991 A
A index.html
笔记
-
此
--no-renames
选项 git lol
用于禁用重命名检测,这样 git 就不会看到删除一个空文件并添加另一个文件作为重命名。大多数时候你不需要它。
-
同样,当您完成了对这个示例存储库的操作后,请记住删除
GIT_AUTHOR_DATE
环境 GIT_COMMITTER_DATE
变量或仅删除 exit
您用来跟随的 shell。
-
考虑通过更新您的
.gitignore
.
简单案例
如果你还没有发布你的历史记录,那么你可以修复它并完成它。有几种方法可以满足你的要求。
git reset --soft
要保留除翻录之外的所有内容(文件内容和提交消息),请首先返回 HEAD
到 DVD 翻录之前的提交,并假装您第一次就做对了。
git reset --soft d29f991
确切的调用将取决于您的本地历史记录。在这种特殊情况下,您可以软重置, HEAD~2
但当您的历史记录具有不同的形状时,盲目地重复这一点会产生令人困惑的结果。
之后添加要保留的文件。软重置不会影响工作树和索引中的文件,因此这些文件 oops.iso
将消失。
git add site.css site.js
您可能能够摆脱 git add .
,特别是如果您更新了 .gitignore
。 这可能是您一开始就陷入麻烦的原因,因此以防万一,请先运行, git status
然后
git commit -q -C ORIG_HEAD
软重置在 处保留一个“书签” ORIG_HEAD
,因此 -C ORIG_HEAD
使用其提交消息。
从这里 git lol --name-status --no-renames
运行
* a19013d (HEAD -> main) C
| A site.css
| A site.js
* d29f991 A
A index.html
git rebase --interactive
为了完成与上述相同的操作但 git
同时引导,请使用交互式变基。
git rebase --interactive d29f991
然后你将看到一个带有
pick 6e90708 B
pick 1982cb8 C
# Rebase d29f991..1982cb8 onto d29f991 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
改为 pick
就 squash
行了 C
。记住:使用交互式变基时,你总是“向上挤压”,而不是向下挤压。
正如下面的有用注释所示,如果简单的话,你可以将行的命令更改 B
为 reword
并在那里编辑提交消息。否则,保存并退出编辑器以获取另一个编辑器,用于压缩 B
和 C
.
git commit-tree
您可能想使用 来执行此 git rebase --onto
操作,但这并不等同于压缩。特别是,如果您意外添加 rip 的提交还包含您想要保留的其他工作,则 rebase 将仅重播其后的提交,因此 site.css
不会随之而来。
在聚会上用 git 管道表演压扁戏法给你的朋友留下深刻印象。
git reset --soft d29f991
git merge --ff-only \
$(git commit-tree 1982cb8^{tree} -p d29f991 \
-F <(git log --format=%s -n 1 1982cb8))
此后的历史就和其他人完全相同了。
* a19013d (HEAD -> main) C
| A site.css
| A site.js
* d29f991 A
A index.html
用英语来说,上面的命令会创建一个新的提交,其树与删除 rip 后得到的树相同( 1982cb8^{tree}
在本例中),但其父级是 d29f991
,然后将当前分支快进到该新提交。
请注意,在实际使用中,您可能希望 %B
整个提交消息正文(而不仅仅是 %s
其主题)采用漂亮的格式。
git filter-repo
以下命令将删除 oop.iso
历史记录中出现的所有内容。
创建存储库的全新克隆并将 cd
其放入其根目录中。插图存储库看起来不像是全新克隆,因此我们必须 --force
在下面的命令中添加选项。
git filter-repo --invert-paths --path oops.iso
由此产生的历史是
* f6c1006 (HEAD -> main) C
| A site.js
* f2498a6 B
| A site.css
* d29f991 A
A index.html
困难案例
如果您确实运行了 git push
,那么您可以执行上述操作之一,但您需要重写历史记录。
您需要选择 git push
覆盖 --force
远程分支或删除分支并再次推送。这两个选项都可能需要远程存储库所有者或管理员的帮助。
不幸的是,这会严重破坏您的合作者。请参阅 “Recovering From Upstream Rebase” in the git rebase
documentation 了解修复历史记录后其他人必须执行的必要步骤。
git filter-branch
(不要使用这个!)
由于历史原因,这个旧命令被保留了下来,但它很慢,而且很难正确使用。只有在万不得已的情况下才使用这条路线。
我在从 Subversion 导入大量二进制测试数据时遇到了类似的问题,并写了一篇关于 从 git 存储库中删除数据的文章 .
执行以下命令
git filter-branch --prune-empty -d /dev/shm/scratch \
--index-filter "git rm --cached -f --ignore-unmatch oops.iso" \
--tag-name-filter cat -- --all
将产生输出
WARNING: git-filter-branch has a glut of gotchas generating mangled history
rewrites. Hit Ctrl-C before proceeding to abort, then use an
alternative filtering tool such as 'git filter-repo'
(https://github.com/newren/git-filter-repo/) instead. See the
filter-branch manual page for more details; to squelch this warning,
set FILTER_BRANCH_SQUELCH_WARNING=1.
Proceeding with filter-branch...
Rewrite 6e907087c76e33fdabe329da7e0faebde165f2c2 (2/3) (0 seconds passed, remaining 0 predicted) rm 'oops.iso'
Rewrite 1982cb83f26aa3a66f8d9aa61d2ad08a61d3afd8 (3/3) (0 seconds passed, remaining 0 predicted)
Ref 'refs/heads/main' was rewritten
各个选项的含义为:
-
--prune-empty
由于过滤操作而 变为空的提交( 即
-
-d
命名一个尚不存在的临时目录,用于构建过滤历史记录。如果您在现代 Linux 发行版上运行,则 tree in /dev/shm
will result in faster execution .
-
--index-filter
是主要事件,并在历史记录中的每个步骤中针对索引运行。您想删除 oops.iso
找到它的任何位置,但它并不存在于所有提交中。 git rm --cached -f --ignore-unmatch oops.iso
当 DVD-rip 存在时,该命令会删除它,否则不会失败。
-
--tag-name-filter
描述如何重写标签名称。 的过滤器 cat
是身份操作。 您的存储库(如上面的示例)可能没有任何标签,但我包含了此选项以实现完全通用性。
-
--
指定选项的结束 git filter-branch
-
--all
以下 --
是所有引用的简写。您的存储库(如上面的示例)可能只有一个引用(主引用),但我包含了此选项以实现完全的通用性。
经过一番搅动,历史现在是这样的:
* f6c1006 (HEAD -> main) C
| A site.js
* f2498a6 B
| A site.css
| * 1982cb8 (refs/original/refs/heads/main) C
| | D oops.iso
| | A site.js
| * 6e90708 B
|/
| A oops.iso
| A site.css
* d29f991 A
A index.html
请注意,新 B
提交仅添加 site.css
,而新 C
提交仅添加 site.js
。标记为 的分支 refs/original/refs/heads/main
包含您的原始提交,以防您犯错。要删除它,请按照 “缩减存储库的检查表”中的步骤操作。
$ git update-ref -d refs/original/refs/heads/main
$ git reflog expire --expire=now --all
$ git gc --prune=now
还有一个更简单的替代方法,就是克隆存储库以丢弃不需要的部分。
$ cd ~/src
$ mv repo repo.old
$ git clone file:///home/user/src/repo.old repo
使用 file:///...
克隆 URL 复制对象而不是仅创建硬链接。
现在你的历史记录是:
* f6c1006 (HEAD -> main) C
| A site.js
* f2498a6 B
| A site.css
* d29f991 A
A index.html