我正尝试从回购中删除一些大型二进制文件,以减小其克隆大小。在研究了主题之后,我偶然发现了以下脚本:
#!/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
按钮,将更改推送到存储库。
再次运行第一个脚本时,我看到文件仍然存在,没有被删除。经过进一步的调查,我发现我在仓库中有两次提交。
我花了一天的时间试图将存储库恢复到旧状态而没有任何运气,与此同时,我也从设备中删除了本地存储库,这意味着我不再拥有git reflog
的历史记录,也不再可以访问类似refs/original/refs/heads/master
之类的东西。
如何将存储库恢复为原始大小?还有可能吗?
答案 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