git filter-branch后跟git push导致两次提交

时间:2018-08-01 15:59:42

标签: git github

我正尝试从回购中删除一些大型二进制文件,以减小其克隆大小。在研究了主题之后,我偶然发现了以下脚本:

#!/bin/bash

# this script displays all blob objects in the repository, sorted from smallest to largest
# you may need `brew install coreutils --with-default-names`

git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| sed -n 's/^blob //p' \
| grep -vF "$(git ls-tree -r HEAD | awk '{print $3}')" \
| awk '$2 >= 2^20' \
| sort --numeric-sort --key=2 \
| gcut -c 1-12,41- \
| gnumfmt --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest

https://stackoverflow.com/a/42544963/5470921进行了一些调整。

输出类似于:

0d99bb931299   44MiB other/assets.sketch
2ba44098e28f   44MiB other/assets.sketch
bd1741ddce0d   45MiB other/assets.sketch

下一步是删除不需要的文件。为此,我使用了以下脚本:

# to remove a file (displayed path/to/file in the output)
git filter-branch --index-filter 'git rm --cached --ignore-unmatch path/to/file' --tag-name-filter cat HEAD

来自https://stackoverflow.com/a/46615578/5470921

到目前为止,一切都很好。接下来,我愚蠢地在master分支上运行以下命令,而没有进行任何备份:

git filter-branch --index-filter 'git rm --cached --ignore-unmatch other/assets.sketch' --tag-name-filter cat HEAD

这创建了一个名为Merge remote-tracking branch 'origin/master'的新提交。之后,我单击GitHub Desktop客户端中的Sync按钮,将更改推送到存储库。

再次运行第一个脚本时,我看到文件仍然存在,没有被删除。经过进一步的调查,我发现我在仓库中有两次提交。

enter image description here enter image description here

我花了一天的时间试图将存储库恢复到旧状态而没有任何运气,与此同时,我也从设备中删除了本地存储库,这意味着我不再拥有git reflog的历史记录,也不再可以访问类似refs/original/refs/heads/master之类的东西。

如何将存储库恢复为原始大小?还有可能吗?

2 个答案:

答案 0 :(得分:1)

注意:如果这是TL; DR,请跳至最后一节如何修复(但是,如果您阅读前面的内容,将会更有意义)。


您需要了解的是git filter-branch 提交。也就是说,它接受每个现有的提交,对其应用一些过滤器或一组过滤器,然后从结果中进行 new 提交。这就是您最终获得两组提交的方式。这是必要的,因为对任何现有提交进行任何更改都没有人(尤其是Git)没有权力。

过滤后的提交是 new 历史记录,很大程度上独立于原始历史记录。 (某些细节取决于精确的过滤器和提交输入。)需要牢记的是,Git存储库不包含文件。它包含 commits ,而这些 commits 是历史记录。每个提交都包含一个快照,因此从某种意义上说,存储库中确实包含文件,但是它们仅位于概述(基于逐个提交)的下方一步。

每个提交都有唯一的哈希ID。这些是您在git log输出中看到的丑陋的大名字:commit b7bd9486b055c3f967a870311e704e3bb0654e4f,依此类推。这个唯一的ID用于Git查找提交对象,从而找到文件;但是哈希ID本身只是提交内容的加密校验和。每个提交也列出其 parent 提交(或多个提交)的哈希ID,并且父哈希(和快照哈希)是提交内容的一部分。这就是为什么 Git无法更改有关提交的任何内容:如果您获取内容,并且更改了任何内容,甚至只有一点,就可以进行新的提交,您将获得一个新的,不同的哈希ID,这是一个新的,不同的提交。

由于每个提交都包含其父级的ID,因此这意味着,如果我们通过哈希ID告诉Git,哪个提交是最新的 ,它可以将该提交撤出并使用找到第二个提交:

...  <--second-newest  <--newest

第二个最新点指向第三个最新点,依此类推。如果链是完全线性的(如果没有分支和合并),我们将得到一个非常简单的图片:

A--B--C--D--E--F--G--H   <-- master

在这里,名称master会记住 latest 提交的实际哈希ID,我们将其称为H,而不是提供其实际哈希ID。提交H记住上一个提交G的哈希ID,它记住F的ID,依此类推。提交A是第一个提交,因此它根本没有父项,这使操作停止。

分支只是选择链中的某些提交并创建不在master顶端的子级的问题。例如,假设我们将master留在原处,指向H,并在我们称为I的新分支上进行新的提交dev

...--H   <-- master
      \
       I   <-- dev (HEAD)

如果我们然后git checkout master并进行新的提交J,则会得到:

...--H--J   <-- master (HEAD)
      \
       I   <-- dev

请注意,将新提交提交到存储库中的行为要求我们让Git更改名称之一。我们放入了新的提交I,并使Git更改了名称dev(该名称以前与H一起指向master),因此dev指向(包含的哈希ID为I。然后我们放入新的提交J,使Git更新master指向J而不是H

(特殊名称HEAD仅附加到我们希望在运行git commit时Git更新的那个分支名称。)

过滤器分支

filter-branch命令遍历某些提交集-通常是 all 提交,具体取决于您如何使用它;您在HEAD(这表示当前分支)上运行了它,但是也许只有一个分支名称master(并复制了它们)。首先,以适当的顺序列出要应用复制过程的每个提交哈希ID。如果您拥有的只是一条线性链(例如A-B-...-H),则这些ID就是该顺序的。为了简单起见,我们假设这一点。

然后,对于每个这样的提交,使用filter-branch:

  • 将提交内容提取到一个临时区域中(或假装以提高速度);
  • 应用您的过滤器;
  • 使用git commit或同等功能(再次取决于过滤器)进行新的提交,该提交将保留每个不变的位,但保留所做的任何更改。

如果新提交与原始提交100%相同,则新哈希ID 是原始哈希ID。假设A本身发生了这种情况:无需进行任何更改,因此Git重用了ID。回购内容现在看起来像这样:

A--B--C--D--E--F--G--H   <-- [original master]
 .
  ...<-- [new master, being built]

然后,Git移至列表中的下一个提交哈希ID,即B。假设这次过滤器进行了一些更改(删除了一个大文件),以便新提交具有一个新的,不同的哈希ID,我们将其称为B'

A--B--C--D--E--F--G--H   <-- [original master]
 \
  B'  <-- [new master, being built]

过滤器分支移至C。即使对C的快照没有任何更改,过滤器分支也会被强制执行 :它必须创建一个新的C',其父节点为{ {1}},因为B'发生了某些事情。现在我们得到B

C'

对所有其余提交重复此操作。他们所有人都获得了新的哈希ID,可能部分是因为快照中的某些内容发生了变化,但肯定是因为其父哈希也发生了变化。最后,A--B--C--D--E--F--G--H <-- [original master] \ B'-C' <-- [new master, being built] 重写 name git filter-branch本身,以指向最终复制的提交master

H'

所有这些都纯粹在您的本地存储库中发生-没有其他Git,原始存储库的克隆都知道发生了任何事情。

(请注意,如果您执行多个过滤分支操作,则每个副本都会复制提交链。某些中间结果可能没有实际价值。Git最终会垃圾收集未使用的和通常无法实现的提交,通常大约一个月后。由于filter-branch复制了内容,您会看到空间使用量增加,而不是减少,直到最终进行垃圾回收并随后重新打包文件为止。)< / p>

哪里出了问题

哪里出了问题绝对不是你想的地方。我认为问题很可能发生在这里:

  

此后,我单击GitHub Desktop客户端中的“同步”按钮

我从未使用过GitHub Desktop软件,所以我不确定它在什么时候起作用。但这很可能在以下情况下发生:

  

[某物]创建了一个名为Merge远程跟踪分支'origin / master'的新提交

因为A--B--C--D--E--F--G--H <-- [original master, now in refs/original/] \ B'-C'-D'-E'-F'-G'-H' <-- master 不会这样做-除非您编写了非常复杂的过滤器,否则不会这样做。 所做的git filter-branch:您连接到另一个仍然具有原始git merge序列的Git,您的Git设置了A-B-...-H来记住他们的{{ 1}},然后您的Git运行合并,将他们的origin/master与您的H连接起来:

H

其中H'是具有两个父级的合并提交

如何修复

既然您拥有的存储库的唯一副本是“双重提交”版本,您将需要做的是:

  • 从该双重版本开始。

  • 使用A--B--C--D--E--F--G--H <-- origin/master \ \ B'-C'-D'-E'-F'-G'-H'-I <-- master I移动您的分支名称,以指向合并两个独立历史记录的合并之前的某些提交。

假设您只有一个git branch -f并且现在已经签出,那么git reset --hard是您的理想之选。 (您只能在没有 附加了master的分支上使用git reset。只能在{em>有的分支上使用git branch -f附加HEAD。)找到您要保留的提交,即经过过滤的提交,它将是合并提交的第一个父提交,并告诉Git命名为{{ 1}}指向该提交,放弃合并。请注意,这将丢失所有未保存的工作;并且这还假设您尚未在合并上方进行任何提交:

git reset

现在图片看起来像这样:

HEAD

基本上与一系列master命令之后的内容相同:唯一的实际不同是,我们显示名称$ git reset --hard HEAD~1 # or HEAD^ 作为您的Git查找提交{{1 }}。 (A--B--C--D--E--F--G--H <-- origin/master \ B'-C'-D'-E'-F'-G'-H' <-- master 上的Git正在使用 its 名称git filter-branch its 存储库中找到提交origin/master。您的Git正在记住他们的H作为您的origin。)

如果一切现在看起来都不错,您剩下的工作就是说服他们的 Git(位于master的那个人)接受新的提交链并移动他们的< / em>名称H,使其指向提交master,即您对原始origin/master所做的最终更正副本。为此,您将使用origin。但是...

如果您只是运行master向他们发送副本,并请求他们更改其H'以指向提交H而不是提交git push,他们会说< em> no 。进行此更改将导致 Git“忘记”或“放弃”提交git push origin master,这将丢失提交master,这将丢失提交H',并且依此类推,一直到您保留的任何提交(如果有)。但是您可以更改您的礼貌请求,请,如果可以,请将您的H 设置为强制命令:设置您的H G

由他们(GitHub)决定是否要遵守 ,但是如果您在GitHub上控制存储库,则显然可以进行设置,使此好。但是请注意,任何拥有原始存储库克隆的 else 仍然具有原始的F提交链。 他们可以合并该链,并礼貌地请求GitHub(或您)接受他们不拥有的提交-它们的合并,以及导致提交master本身的所有内容,然后合并它回到你的主人。因此,即使您故意丢弃这些提交,它们也可以很容易地回来困扰您。

(在Git中很难永远摆脱某些东西。这通常被认为是一个功能。)

答案 1 :(得分:0)

根据@torek的答案,以下是我将解决此问题的步骤,我将在今天晚些时候执行此步骤,并使用结果(或进行编辑,如有修改)更新此答案,仅供参考。

# make sure the current branch is the one with the duplicates, in this case it's `master`
git checkout master

# double check you are on `master`
git status

# create a new branch from `master`
git checkout -b fix-duplicates

# double check you are on `fix-duplicates`
git status

# .. -A-B- .. -C-D-E- .. -F
#      \        /
#       B- .. -C

# A = aaaaaaaa, branching starts
# B = bbbbbbbb, branching takes effect (one commit after where it started in A)
# C = cccccccc, branching ends (exclude the merge commit that cause duplicates D)
# E = eeeeeeee, one commit after the merge commit
# F = ffffffff, most recent commit

# move back to the point where the branching started
git reset --hard A

# 1) to cherry pick with new commit dates
# cherry pick all commits from where the branching started up to where the branching ends
# exclude the merge commit at the top (the one that caused the duplication)
git cherry-pick B..C

# cherry pick all commits after the the merge up to most recent commit
git cherry-pick E..F

# 2) if you want to keep the original dates, run the following scripts instead
for commit in $(git rev-list B..C)
do
    export GIT_COMMITTER_DATE=$(git log -1 --format='%at' $commit)
    git cherry-pick $commit
done

for commit in $(git rev-list E..F)
do
    export GIT_COMMITTER_DATE=$(git log -1 --format='%at' $commit)
    git cherry-pick $commit
done

# make sure the fix is good by comparing the two branches, they should be identical
git diff master..fix-duplicates

# make the fixed branch the new `master`
git checkout master
git reset --hard fix-duplicates

# review what you did (optional)
git reflog

# forcefully push the changes (make sure everything is right before this step!)
git push -f origin master