为什么git在远程解析增量之前将git对象写入远程

时间:2019-03-14 03:26:59

标签: git version-control push

一个简短的问题:我发现git在git push --force期间,在远程解析增量之前,git会将blob对象写入远程,即使不久前将相同的blob对象写入了相同的远程存储库。

我想问:

  1. 为什么git将静态blob对象写入远程对象,即使后者具有远程对象
  2. 是否可以停止git(客户端或服务器端)

更长的故事:

我有一个既包含静态文件又包含代码的存储库,并且对它们进行了不同的管理。

所有代码文件都在分支“ history”中,所有静态文件都在分支“ static”中,分支“ history”和“ static”共享一个共同的初始提交,它们合并形成分支“ master”,如下图所示:

*   commit (HEAD -> master, origin/master)
|\  Merge:
| |
| |
| | 
| |     Merge branch 'static'
| | 
| * commit (static)
| |
| |
| | 
| |
| | 
* | commit (origin/history, history)
| |
| | 
| |
| |     
| |
| | 
* | commit
| |
| |
| | 
| |
| | 
* | commit
|/
|
|   
|
| 
* Initial commit

每次都有代码更新时,我将更改提交到分支“ master”中,然后将提交重新建立到分支“ history”上,然后签出分支“ history”并再次出现分支“ static”,在此过程中,分支“历史”(快进)和“大师”(强制更新)被推送到远程:

git rebase --onto history origin/master master
commit=`git rev-parse HEAD`
git checkout $history_branch
git reset --hard $commit

git push

git checkout master
git reset --hard history
git merge -m "Merge branch 'static'" static

git push --force

此命令执行速度更快,因为它不会将静态文件传输到包含大文件的远程文件。

当静态文件发生更改时,我签出分支“ static”,使用--amend标志提交更改,然后签出分支“ history”并合并分支“ static”,强制更新分支“ master”在远程过程结束时:

git checkout static
git add .
git commit --amend -m 'Add static files'

# As torek pointed out, I made a mistake in this post
# The following "git push" command is not performed
# git push

git checkout master
git reset --hard history
git merge -m "Merge branch 'static'" static

# git push --force
git push --force origin static master

# torek's suggestion "What you can do about this, part 1"
# does not work out for me:
#
# $ git push --force origin static master
# Counting objects: 422, done.
# Compressing objects: 100% (407/407), done.
# Writing objects: 100% (422/422), 480.08 MiB | 1.05 MiB/s, done.
# Total 422 (delta 41), reused 0 (delta 0)
# remote: Resolving deltas: 100% (41/41), completed with 1 local object.
# To ...
#  + 3539524...6618427 master -> master (forced update)
#  + 6a1f0c0...ba60bb9 static -> static (forced update)


但是,最后一条命令需要很长时间才能完成,我发现git在远程解析增量之前将所有静态blob对象写入远程:

Counting objects: 422, done.
Compressing objects: 100% (407/407), done.
Writing objects: 100% (422/422), 480.08 MiB | 1.44 MiB/s, done.
Total 422 (delta 41), reused 0 (delta 0)
remote: Resolving deltas: 100% (41/41), completed with 1 local object.

即使第二次执行命令,也会发生这种情况,并且在第一次执行和第二次执行之间不会对工作树进行任何修改。

我使用了how-does-gits-transfer-protocol-work中的脚本来列出第二次执行之前和之后本地存储库中的所有对象,结果显示第二次执行之后只有2个新对象,即{{1 }}和git commit --amend -m 'Add static files',这意味着不会创建新的Blob对象。

其他信息:

以下是负责工作流程的脚本:

git merge -m "Merge branch 'static'" static

客户端git版本:2.17.1 客户端操作系统:18.04.2 LTS(Bionic Beaver),x86_64,virtualbox 5.2.26中的VM 服务器git版本:2.11.0 服务器操作系统:Debian GNU / Linux 9(拉伸),x86_64

本地存储库是客户端操作系统本地磁盘上的目录,后者移至virtualbox共享文件夹内的目录,结果相同。


编辑:在经历了所有麻烦之后,我决定接受torek的第二条建议,而不是重写历史记录。如果过时的静态文件占用太多空间,我仍然需要压缩提交,因此我将所有代码文件都移到了子树中,并从那里进行管理:

#!/bin/bash

master_branch=master
master_origin=origin/master
history_branch=history
static_branch=static


make_master() {
    git checkout $master_branch
    git reset --hard $history_branch
    git merge -m "Merge branch 'static'" $static_branch
}


extend_history() {
    git rebase --onto $history_branch $master_origin $master_branch
    local commit=`git rev-parse HEAD`
    git checkout $history_branch
    git reset --hard $commit
}


add_static() {
    git checkout $static_branch
    git add .
    git commit --amend -m 'Add static files'
}


case "$1" in
code)
    extend_history
    git push
    make_master
    git push --force
;;
asset)
    add_static
    make_master
    # git push --force
    git push --force origin $static_branch $master_branch
;;
*)
    echo "Unknown action \"$1\"" >&2
    exit 127
esac

要压缩静态提交:

git checkout static
git subtree add -P code history
git checkout master
git reset --hard static
# Remove branch static and history, their tracking branches,
# and their counterparts in remote repository

1 个答案:

答案 0 :(得分:1)

Git 可以确实做正确的事情:它可能会询问服务器您是否有Blob H来获取哈希值H ,并且如果服务器已经拥有它,请避免再次发送。

尽管如此,Git实际上并没有这样做。好吧,无论如何,“好”在某种程度上还是不错的。 Git的作用是询问服务器是否具有特定的 commits 。然后,根据结果做出一些合理但不一定100%准确的假设。有时这将意味着不必要地发送对象。而且,并非完全偶然,实现推送的代码并没有实现您在解释该代码之前所做的声明。 (这是我认为,是问题的根源,但我尚未对此进行测试。)

仍然,您可以做一些事情。首先,让我们看一下Git在做什么。

详细信息

  

当静态文件发生更改时,我签出分支“ static”,使用--amend标志提交更改,然后签出分支“ history”并合并分支“ static”,强制更新分支“ master”在远程过程结束时:

git checkout static
git add .
git commit --amend -m 'Add static files'

这时,在您自己的存储库中,您具有:

       R    [static@{1}]
      /
...--o--S   <-- static

(尽管实际上...部分为空,而o是下面的A提交)。

提交R曾经是static的尖端; S作为static的新提示已被抛在一边。这两个提交都确实存在于您自己的存储库中。

git push

您没有执行此步骤。因此,服务器尚未提交S。 (查看案例asset的代码,该案例先运行add_static,然后运行make_master,然后运行git push --forcemake_master步骤将当前分支设置为{{1 }},因此master仅推送 git push --force。这就是master输出不显示git log --graph的原因。)如果这样做,您会需要在这里origin/static

我们现在继续:

git push --force

我们也绘制这张图,包括推开的先前的git checkout master git reset --hard history git merge -m "Merge branch 'static'" static git push --force (之所以master@{2},是因为我们有两个干预事件:重置,然后合并)。该图反映了您的存储库中的内容,如下所示:

@{2}

(commit R--------M <-- origin/master, master@{2} / / A--o--o--L <-- history, origin/history, master@{1} \ \ S--------N <-- master 带有R标签,而static@{1}带有Sstatic;出于空间原因,我不在图形中包括这些标签)。

与此同时,服务器具有以下任一功能:

origin/static

这是使事情变得有趣的地方。客户端现在必须确定要发送的对象。通过启动与服务器的对话来实现。它以:我想给您发送 R--------M <-- master / / A--o--o--L <-- history ;您有N吗?当然,由于您刚刚完成提交,服务器没有提交N

由于服务器拒绝,客户端会说:然后,我需要您拥有N的父母NL;你有那些吗?当然,他们有S,但没有L。客户端现在知道要发送SN,并且服务器具有与S关联的所有对象-并且由于服务器上的历史记录不是 浅,链中的所有对象都从LL

客户端现在询问服务器是否具有A的父节点S,或者假定服务器是A的祖先,因为它是A的祖先;无论哪种方式,它最终都会意识到服务器确实具有L

客户端现在假设服务器具有服务器提到的所有提交中的所有对象。因为没有/没有协议交换中没有提到A,所以假定服务器上存在提交R。因此,它将所有R中的对象打包并发送。服务器对此进行重新打包,发现大多数Blob是冗余的,并有效地忽略了这些冗余Blob。

您可以为此做些什么,第1部分

一种解决方法是继续在服务器上设置与提交S相对应的标签(在较早的步骤)。也就是说,添加R,以便git push --force origin static的标签origin指向static

然后,在向他们发送R的新提交时,请确保告诉他们同时更新 master {{1} }:

static

或:

master

(它们的意思是相同的-refspec上的加号设置该特定refspec的force标志,在这种情况下,我喜欢明确性,但是您可以使用任何喜欢的语法)。

现在服务器将具有:

git push --force origin static master

,并将宣传其git push origin +static:static +master:master 表示提交 ...........<-- static . R--------M <-- master / / A--o--o--L <-- history 的事实。客户端需要此信息来为其预推送挂钩(无论它是否实际运行任何预推送挂钩)。因此,当客户端发送新的提交时,它将提供发送refs/heads/static(用于更新R,因为它在更新的S的历史记录中)和static(用于更新master),但但是,这一次它可以告诉服务器具有N应该只能发送一个新的Blob。

(我不确定它会这样做,但是应该很容易进行测试。)

请注意,您必须同时完成这两个推送,因为服务器一旦接受master作为其RS作为其static,将同时垃圾回收Nmaster。 (服务器通常未启用reflog,并且所有这些对象都位于打包文件中,因此不受对象松散的14天宽限期的限制。)

您可以为此做些什么,第2部分

另一个选择是完全停止重写历史记录。您可能不喜欢此选项,因为您的静态资产对象会随着时间累积,从而扩大存储库的大小。但这也将彻底解决该问题,因为现在客户端可以正确理解服务器的历史记录。

从某种意义上讲,是历史记录重写引起了问题:客户端做出假设,因为服务器没有任何静态资产对象,因为 除根提交M以外,该分支上的每个新提交都与其他内容完全无关。这种假设是“安全的”,因为它只会导致发送额外的对象。它节省了大量的时间,因为枚举每个提交后面的所有树和blob对象非常慢-只需说: Aha,服务器具有此提交,所以速度要快得多,因此-除了对于浅层嫁接所引入的复杂性,我们将在这里忽略-它具有通过此提交及其历史记录隐含的所有对象。客户端几乎不需要提供任何哈希ID,因为服务器很快会以是的,我已经有一个,并且终止了遍历图的该部分。如果服务器具有R,则它也具有A之前的所有内容。如果它有L,则它包含L之前的所有内容。

好吧,我应该修正一下:它会节省很多时间,除了您正在重写历史记录,这样客户端就不会再询问了约R。所有对象的完整枚举虽然很慢,但可能比通过提交R重新发送大多数对象要快。当然可以节省一些带宽。但是对于大多数正常情况,对于不做大量重写的Git历史记录,以Git枚举提交并仅假设有关这些提交背后的树和斑点的方式来执行此操作会更快。 / p>