git 如何区分已删除的分支和新的、未推送的本地分支?
How does git differentiate between deleted branches and new, unpushed local branches?
是否可以在本地结帐时修剪从远程删除的所有分支,同时保持未推送的本地分支完好无损?从远程删除分支如何传播到克隆和分叉?
TL;DR
您可能想要设置 fetch.prune
,然后避免创建任何您不打算使用的 b运行ch 名称。使用 remote-tracking 个名字(refs/remotes/origin/*
个名字)查看其他人的提交。
长
这里确实嵌入了几个不同的问题,所以让我们一次一个地回答它们:
How does git differentiate between deleted branches and new, unpushed local branches?
不需要,但确实不需要。
Is it possible to prune all branches deleted from the remote on my local checkout while leaving my unpushed, local branches intact?
也许吧,看你的意思。
How does branch deletion from the remote propagate to clones and forks?
没有。
所有这些问题都源于我认为是某种 category error 的问题:您认为 b运行 就好像它们 很重要 .更准确地说,您认为 b运行ch names 是全局共享的,但它们不是。
提交
Git 真的是关于 提交 .
每个提交都有编号,这些 编号 是全局共享的:每个 Git 都以相同的方式计算它们。实际计算——以及提交哈希 ID 如此大和丑陋的原因——是内部 Git 对象的完整内容的加密校验和(校验和是未压缩数据的,尽管对象总是存储在一些压缩形式)。
每个提交由两部分组成。提交中的主要数据包含 Git 知道的每个文件的完整快照,无论谁提交,都进行了提交。提交中的 元数据 包含有关提交的信息:提交人和时间、日志消息,以及对 Git 本身至关重要的提交编号或哈希 ID,前一个提交的 Git 调用该提交的 parent。 (合并提交通过具有两个或更多父项来区分。)
这个编号系统意味着只要你知道某个链中last提交的哈希ID,Git就可以用它来从最后一次提交到每个较早的提交工作向后。 (它还有一些令熟悉大多数其他版本控制系统的人感到惊讶的后果,例如:许多提交同时在多个 b运行ches 上,并且包含一个 b运行ches 的集合动态提交更改。)
姓名,包括 b运行ch 姓名
这导致 b运行ch 名称对 Git(和我们)所做的事情:他们只记住 last 提交的哈希 ID连锁,链条。我们可以这样画:
... <-F <-G <-H <--name
该名称允许 Git 快速找到最后一次提交 H
的哈希 ID。 H
中的元数据包括早期提交 G
的哈希 ID,它允许 Git 找到 G
。提交 G
包括早期提交 F
的 has ID,依此类推。
标签名称和其他 Git 名称做同样的事情:每个名称的形式通常为 refs/<em>group</em>/ <em>name</em>
,映射到一个哈希ID。这里的group
对于b运行ch名称是heads
,对于标签名称是tags
,并且包括remotes
对于remote-tracking 个名字,我们稍后会讲到。何时以及是否这样做从命名提交开始,然后向后工作取决于每个Git命令:git log
,例如,几乎总是这样做,而 git rev-parse
,例如,不会。
因此每个存储库由两个 key-value databases:
通过克隆复制的主要对象以哈希 ID 作为键,以内部 Git 对象作为值。键只是值的校验和,它提供了一致性检查:当 Git 查找值时,它计算校验和,它必须与用于查找值的键匹配。
辅助数据库不(必然)重复;它从名称映射到哈希 ID。
克隆、获取和 remote-tracking 名称
当您使用 git clone
复制 Git 存储库时,您的 Git 会从其他 Git 获取提交。您的 Git 有他们的 Git find 使用他们的 b运行ch 名称获取的提交。您的 Git 知道您是否有任何这些提交但使用它们的哈希 ID,当然 git clone
首先是创建一个空存储库。稍后的 git fetch
执行与 git clone
相同的获取,但这次您可能确实有一些提交。
您可以随时 运行 git ls-remote origin
查看 git fetch origin
将看到的 names-and-hash-IDs 列表。 (尝试一下;这是一个 read-only 操作。)这实际上只是 git fetch
的第一步,它在开始 commit-copying进程。
获取了 new-to-your-Git 提交——作为 git clone
的中间部分,或者使用 git fetch
——你的 Git 现在知道它们的 b运行ch 名称和哈希 ID,以及 o她的名字,并用它来创建或更新你的 Git的名字。对于每个 b运行ch 名称 refs/heads/<em>B</em>
,您的Git 将名称更改为 refs/remotes/<em>remote</em>/<em>B</em>
,其中 remote
是您用来与其他 Git 交谈的名称。对于 git clone
,这通常是 origin
(尽管您可以设置它);在 git remote add
之后,您可以选择一个名称,您可以 运行 git fetch
使用您选择的名称。
因此,假设您使用的是远程名称 origin
,这会将他们的 refs/heads/master
变成您的 refs/remotes/origin/master
,并将他们的 refs/heads/dev
变成您的 [=49] =].因为这些名称以 refs/remotes
开头,所以它们是 remote-tracking 名称 ,而不是 b运行ch 名称。您的 Git 根据它看到的所有 b运行ch 名称创建或更新这些名称中的每一个。1 所以您最终得到一个 remote-tracking您 Git 看到的每个 b运行ch 名称的名称。
你自己git clone
的最后步是运行git checkout
。2通常,这将在您的新存储库中创建一个 b运行ch 名称。 checkout
(或者在 Git 2.23 和更高版本中,switch
)命令有一个特殊的功能,如果你命名一个 b运行ch 但实际上没有 有 b运行ch 名称,他们创建 使用相应的remote-tracking 名称的名称。由于您的 Git 刚刚创建了所有 remote-tracking 名称,因此您将在此处恰好有一个正确的对应名称,并且您的 Git 将创建该名称,指向与 b[= 相同的提交435=]ch 名称,您的 remote-tracking 名称源自该名称。3
1对所有引用有一个一般要求——这些refs/*
名字是引用——它们必须指向存储库中一些现有的有效 Git 对象。 B运行ch 和 remote-tracking 名称被进一步限制为仅指向 commit 对象,而标签名称可以指向四种内部对象类型中的任何一种。您可以指示 git fetch
仅获取提交的某些子集,如果是这种情况,您的 git fetch
将无法创建或更新现在指向您实际上并不指向的对象的名称有。它只是根据需要跳过这些。
2或git switch
,但其实这都是直接内置到git clone
,所以只是用代码,而不是前面端接口。您也可以使用 git clone --no-checkout
跳过此步骤。
3您可以通过多种方式解决这个问题。当然最简单的是脚注2中提到的--no-checkout
选项,但是你也可以克隆一个没有b运行ch名称的仓库,或者使用带有-b
选项的标签名称来git clone
。我们真的不需要在这里介绍这些情况下发生的事情,但是一旦您了解了潜在的机制,这就不足为奇了。
镜像克隆和 GitHub-style 分支
您还可以运行 git clone --bare
,我们不会在这里适当地介绍,或者git clone --mirror
; GitHub 和其他托管服务提供“分叉”存储库的选项,这与使用 git clone --mirror
非常相似,只是有所不同,并且有一些我们也不会在此处介绍的额外功能。
A bare 克隆只是一个没有工作树的克隆。提交中的文件都被冻结并且read-only,实际上不可能对它们做任何新的工作,所以对于一个普通的克隆,你会得到一个工作树,在那里你有文件你可以 查看、读取、写入和以其他方式用于完成工作。裸克隆忽略了这一点。省略工作区的目的是让裸克隆可以安全地接收 git push
操作。在 non-bare 克隆中,如果 Git 允许,推送操作可以断开正在进行的工作与 b运行ch 的连接(由于我们无法在此处正确涵盖的原因使答案更短)。
A mirror 克隆是一个裸克隆,其中 Git 复制所有 b运行ch 而不是创建 remote-tracking 名称名字 as-is。也就是说,如果源存储库有一个 refs/heads/dev
,则克隆也有一个 refs/heads/dev
。记录此镜像状态,以便镜像克隆 中的后续 git fetch
将其先前的 branch-to-hash-ID 映射替换为来自另一个 Git 的新映射 .这意味着不仅不可能在镜像克隆中做任何新工作,而且 git push
到 镜像克隆通常也是一个坏主意,因为推送给它的东西会输给了下一个 git fetch
.
A GitHub 或其他 hosing 服务 fork 使用可能也是镜像克隆的东西来制作初始副本:新克隆具有相同的一组b运行ch 命名为原始克隆,并且是裸的,以便它可以接收推送操作。但是这个分叉副本从来没有 git fetch
运行 在 它,所以 运行 git push
是安全的它。 (托管服务器还设置了一些 outside-of-Git-itself 链接,以便您可以进行拉取请求。这些拉取请求不是Git 特点:它们是在托管服务器上实现的,每个都有自己不同的怪癖。)
正在删除 b运行ch 名称和 fetch.prune
现在我们知道 b运行ch 名称本身对于每个存储库都是本地的,我们可以了解删除 b运行ch 名称是如何工作的。我们可以从例如:
...--G--H <-- master
\
I--J <-- dev
我们使用一些 Git 操作使名称 master
指向提交 J
,这样通过 J
的所有提交现在都在 both b运行ches:
...--G--H--I--J <-- dev, master
现在我们不再需要 name dev
,所以我们将其删除:
...--G--H--I--J <-- master
这就是它真正的全部,至少,本地。
但是其他一些 Git 将我们的 refs/heads/dev
复制到他们的 refs/remotes/origin/dev
呢?现在我们的 dev
已经完成,他们的 origin/dev
......嗯,默认情况下,它会保留下来。这可能是错误的默认设置,但这是 Git 最初所做的。
假设 我们 是 origin/dev
的人。我们有,在我们的 Git:
K <-- feature (HEAD)
/
...--G--H <-- master, origin/master
\
I--J <-- origin/dev
我们不需要 dev
所以我们不创建它;我们创建了 feature
并在其上提交了 K
。但是现在origin
的dev
不见了。我们可以做的一件事是 运行:
git fetch --prune
或者,更方便的是,将 fetch.prune
配置为 true
(在此存储库中或在我们的全局 per-user Git 配置中,使用 git config --global
)。这告诉我们的Git:当我运行git fetch
时,就好像我运行git fetch --prune
一样,这告诉我们Git:当你得到他们所有 b运行ches 的列表时,使用该列表删除任何refs/remotes/origin/*
名称在另一个 Git 上没有相应的 b运行ch 名称(如果不是 origin
,请替换为适当的遥控器)。
我们在本地最终得到的是这样的:
K <-- feature (HEAD)
/
...--G--H <-- master
\
I--J <-- origin/master
由于我们从未创建本地 dev
,因此我们不必删除本地 dev
。我们只是 master
过时了。如果我们不 喜欢 有一个过时的 master
,我们可以删除我们的名字 master
。我们不再需要它了:我们有我们正在处理的 feature
,这是我们 做 需要的名字,用来记录我们未推送的提交。
回顾
理解所有这些的关键是我们不需要关心未推送的 b运行ches。我们需要关心未推送的 提交 。在我们的存储库中,我们的 b运行ch 名称用于跟踪 last 提交:这就是 b运行ch 名称为我们所做的。当我们“在”一些 b运行ch 时——因为 git status
说 在 b运行ch <em>whatever</em>
—和运行 git commit
,新提交将获得一个新的唯一编号,然后Git将自动更新那个b运行ch名称 以便它记录新提交的 hash-ID 编号,而不是旧的 branch-tip.
如果我们去 git push
一些提交到其他 Git 存储库,我们将:
- 让我们的 Git 通过哈希 ID 将提交发送到他们的 Git;然后
- 让我们的 Git 让他们的 Git 设置一个 他们的 b运行ch 名称来记录 这些提交的最后。
最后一步可以在其他 Git 中创建或更新 b运行ch,为了理智起见,我们通常喜欢在每个 Git 中使用相同的名称。但实际上不需要。共享的部分是提交本身及其唯一的哈希 ID。他们有自己的 b运行ch 名字,我们也有自己的名字。我们将他们的 b运行ch 名称复制到我们的 remote-tracking 名称中,以便我们可以找到他们拥有但我们没有 b运行ch 的任何提交 名称,但在我们自己进行新提交之前,我们不需要任何 b运行ch 名称。
是否可以在本地结帐时修剪从远程删除的所有分支,同时保持未推送的本地分支完好无损?从远程删除分支如何传播到克隆和分叉?
TL;DR
您可能想要设置 fetch.prune
,然后避免创建任何您不打算使用的 b运行ch 名称。使用 remote-tracking 个名字(refs/remotes/origin/*
个名字)查看其他人的提交。
长
这里确实嵌入了几个不同的问题,所以让我们一次一个地回答它们:
How does git differentiate between deleted branches and new, unpushed local branches?
不需要,但确实不需要。
Is it possible to prune all branches deleted from the remote on my local checkout while leaving my unpushed, local branches intact?
也许吧,看你的意思。
How does branch deletion from the remote propagate to clones and forks?
没有。
所有这些问题都源于我认为是某种 category error 的问题:您认为 b运行 就好像它们 很重要 .更准确地说,您认为 b运行ch names 是全局共享的,但它们不是。
提交
Git 真的是关于 提交 .
每个提交都有编号,这些 编号 是全局共享的:每个 Git 都以相同的方式计算它们。实际计算——以及提交哈希 ID 如此大和丑陋的原因——是内部 Git 对象的完整内容的加密校验和(校验和是未压缩数据的,尽管对象总是存储在一些压缩形式)。
每个提交由两部分组成。提交中的主要数据包含 Git 知道的每个文件的完整快照,无论谁提交,都进行了提交。提交中的 元数据 包含有关提交的信息:提交人和时间、日志消息,以及对 Git 本身至关重要的提交编号或哈希 ID,前一个提交的 Git 调用该提交的 parent。 (合并提交通过具有两个或更多父项来区分。)
这个编号系统意味着只要你知道某个链中last提交的哈希ID,Git就可以用它来从最后一次提交到每个较早的提交工作向后。 (它还有一些令熟悉大多数其他版本控制系统的人感到惊讶的后果,例如:许多提交同时在多个 b运行ches 上,并且包含一个 b运行ches 的集合动态提交更改。)
姓名,包括 b运行ch 姓名
这导致 b运行ch 名称对 Git(和我们)所做的事情:他们只记住 last 提交的哈希 ID连锁,链条。我们可以这样画:
... <-F <-G <-H <--name
该名称允许 Git 快速找到最后一次提交 H
的哈希 ID。 H
中的元数据包括早期提交 G
的哈希 ID,它允许 Git 找到 G
。提交 G
包括早期提交 F
的 has ID,依此类推。
标签名称和其他 Git 名称做同样的事情:每个名称的形式通常为 refs/<em>group</em>/ <em>name</em>
,映射到一个哈希ID。这里的group
对于b运行ch名称是heads
,对于标签名称是tags
,并且包括remotes
对于remote-tracking 个名字,我们稍后会讲到。何时以及是否这样做从命名提交开始,然后向后工作取决于每个Git命令:git log
,例如,几乎总是这样做,而 git rev-parse
,例如,不会。
因此每个存储库由两个 key-value databases:
通过克隆复制的主要对象以哈希 ID 作为键,以内部 Git 对象作为值。键只是值的校验和,它提供了一致性检查:当 Git 查找值时,它计算校验和,它必须与用于查找值的键匹配。
辅助数据库不(必然)重复;它从名称映射到哈希 ID。
克隆、获取和 remote-tracking 名称
当您使用 git clone
复制 Git 存储库时,您的 Git 会从其他 Git 获取提交。您的 Git 有他们的 Git find 使用他们的 b运行ch 名称获取的提交。您的 Git 知道您是否有任何这些提交但使用它们的哈希 ID,当然 git clone
首先是创建一个空存储库。稍后的 git fetch
执行与 git clone
相同的获取,但这次您可能确实有一些提交。
您可以随时 运行 git ls-remote origin
查看 git fetch origin
将看到的 names-and-hash-IDs 列表。 (尝试一下;这是一个 read-only 操作。)这实际上只是 git fetch
的第一步,它在开始 commit-copying进程。
获取了 new-to-your-Git 提交——作为 git clone
的中间部分,或者使用 git fetch
——你的 Git 现在知道它们的 b运行ch 名称和哈希 ID,以及 o她的名字,并用它来创建或更新你的 Git的名字。对于每个 b运行ch 名称 refs/heads/<em>B</em>
,您的Git 将名称更改为 refs/remotes/<em>remote</em>/<em>B</em>
,其中 remote
是您用来与其他 Git 交谈的名称。对于 git clone
,这通常是 origin
(尽管您可以设置它);在 git remote add
之后,您可以选择一个名称,您可以 运行 git fetch
使用您选择的名称。
因此,假设您使用的是远程名称 origin
,这会将他们的 refs/heads/master
变成您的 refs/remotes/origin/master
,并将他们的 refs/heads/dev
变成您的 [=49] =].因为这些名称以 refs/remotes
开头,所以它们是 remote-tracking 名称 ,而不是 b运行ch 名称。您的 Git 根据它看到的所有 b运行ch 名称创建或更新这些名称中的每一个。1 所以您最终得到一个 remote-tracking您 Git 看到的每个 b运行ch 名称的名称。
你自己git clone
的最后步是运行git checkout
。2通常,这将在您的新存储库中创建一个 b运行ch 名称。 checkout
(或者在 Git 2.23 和更高版本中,switch
)命令有一个特殊的功能,如果你命名一个 b运行ch 但实际上没有 有 b运行ch 名称,他们创建 使用相应的remote-tracking 名称的名称。由于您的 Git 刚刚创建了所有 remote-tracking 名称,因此您将在此处恰好有一个正确的对应名称,并且您的 Git 将创建该名称,指向与 b[= 相同的提交435=]ch 名称,您的 remote-tracking 名称源自该名称。3
1对所有引用有一个一般要求——这些refs/*
名字是引用——它们必须指向存储库中一些现有的有效 Git 对象。 B运行ch 和 remote-tracking 名称被进一步限制为仅指向 commit 对象,而标签名称可以指向四种内部对象类型中的任何一种。您可以指示 git fetch
仅获取提交的某些子集,如果是这种情况,您的 git fetch
将无法创建或更新现在指向您实际上并不指向的对象的名称有。它只是根据需要跳过这些。
2或git switch
,但其实这都是直接内置到git clone
,所以只是用代码,而不是前面端接口。您也可以使用 git clone --no-checkout
跳过此步骤。
3您可以通过多种方式解决这个问题。当然最简单的是脚注2中提到的--no-checkout
选项,但是你也可以克隆一个没有b运行ch名称的仓库,或者使用带有-b
选项的标签名称来git clone
。我们真的不需要在这里介绍这些情况下发生的事情,但是一旦您了解了潜在的机制,这就不足为奇了。
镜像克隆和 GitHub-style 分支
您还可以运行 git clone --bare
,我们不会在这里适当地介绍,或者git clone --mirror
; GitHub 和其他托管服务提供“分叉”存储库的选项,这与使用 git clone --mirror
非常相似,只是有所不同,并且有一些我们也不会在此处介绍的额外功能。
A bare 克隆只是一个没有工作树的克隆。提交中的文件都被冻结并且read-only,实际上不可能对它们做任何新的工作,所以对于一个普通的克隆,你会得到一个工作树,在那里你有文件你可以 查看、读取、写入和以其他方式用于完成工作。裸克隆忽略了这一点。省略工作区的目的是让裸克隆可以安全地接收 git push
操作。在 non-bare 克隆中,如果 Git 允许,推送操作可以断开正在进行的工作与 b运行ch 的连接(由于我们无法在此处正确涵盖的原因使答案更短)。
A mirror 克隆是一个裸克隆,其中 Git 复制所有 b运行ch 而不是创建 remote-tracking 名称名字 as-is。也就是说,如果源存储库有一个 refs/heads/dev
,则克隆也有一个 refs/heads/dev
。记录此镜像状态,以便镜像克隆 中的后续 git fetch
将其先前的 branch-to-hash-ID 映射替换为来自另一个 Git 的新映射 .这意味着不仅不可能在镜像克隆中做任何新工作,而且 git push
到 镜像克隆通常也是一个坏主意,因为推送给它的东西会输给了下一个 git fetch
.
A GitHub 或其他 hosing 服务 fork 使用可能也是镜像克隆的东西来制作初始副本:新克隆具有相同的一组b运行ch 命名为原始克隆,并且是裸的,以便它可以接收推送操作。但是这个分叉副本从来没有 git fetch
运行 在 它,所以 运行 git push
是安全的它。 (托管服务器还设置了一些 outside-of-Git-itself 链接,以便您可以进行拉取请求。这些拉取请求不是Git 特点:它们是在托管服务器上实现的,每个都有自己不同的怪癖。)
正在删除 b运行ch 名称和 fetch.prune
现在我们知道 b运行ch 名称本身对于每个存储库都是本地的,我们可以了解删除 b运行ch 名称是如何工作的。我们可以从例如:
...--G--H <-- master
\
I--J <-- dev
我们使用一些 Git 操作使名称 master
指向提交 J
,这样通过 J
的所有提交现在都在 both b运行ches:
...--G--H--I--J <-- dev, master
现在我们不再需要 name dev
,所以我们将其删除:
...--G--H--I--J <-- master
这就是它真正的全部,至少,本地。
但是其他一些 Git 将我们的 refs/heads/dev
复制到他们的 refs/remotes/origin/dev
呢?现在我们的 dev
已经完成,他们的 origin/dev
......嗯,默认情况下,它会保留下来。这可能是错误的默认设置,但这是 Git 最初所做的。
假设 我们 是 origin/dev
的人。我们有,在我们的 Git:
K <-- feature (HEAD)
/
...--G--H <-- master, origin/master
\
I--J <-- origin/dev
我们不需要 dev
所以我们不创建它;我们创建了 feature
并在其上提交了 K
。但是现在origin
的dev
不见了。我们可以做的一件事是 运行:
git fetch --prune
或者,更方便的是,将 fetch.prune
配置为 true
(在此存储库中或在我们的全局 per-user Git 配置中,使用 git config --global
)。这告诉我们的Git:当我运行git fetch
时,就好像我运行git fetch --prune
一样,这告诉我们Git:当你得到他们所有 b运行ches 的列表时,使用该列表删除任何refs/remotes/origin/*
名称在另一个 Git 上没有相应的 b运行ch 名称(如果不是 origin
,请替换为适当的遥控器)。
我们在本地最终得到的是这样的:
K <-- feature (HEAD)
/
...--G--H <-- master
\
I--J <-- origin/master
由于我们从未创建本地 dev
,因此我们不必删除本地 dev
。我们只是 master
过时了。如果我们不 喜欢 有一个过时的 master
,我们可以删除我们的名字 master
。我们不再需要它了:我们有我们正在处理的 feature
,这是我们 做 需要的名字,用来记录我们未推送的提交。
回顾
理解所有这些的关键是我们不需要关心未推送的 b运行ches。我们需要关心未推送的 提交 。在我们的存储库中,我们的 b运行ch 名称用于跟踪 last 提交:这就是 b运行ch 名称为我们所做的。当我们“在”一些 b运行ch 时——因为 git status
说 在 b运行ch <em>whatever</em>
—和运行 git commit
,新提交将获得一个新的唯一编号,然后Git将自动更新那个b运行ch名称 以便它记录新提交的 hash-ID 编号,而不是旧的 branch-tip.
如果我们去 git push
一些提交到其他 Git 存储库,我们将:
- 让我们的 Git 通过哈希 ID 将提交发送到他们的 Git;然后
- 让我们的 Git 让他们的 Git 设置一个 他们的 b运行ch 名称来记录 这些提交的最后。
最后一步可以在其他 Git 中创建或更新 b运行ch,为了理智起见,我们通常喜欢在每个 Git 中使用相同的名称。但实际上不需要。共享的部分是提交本身及其唯一的哈希 ID。他们有自己的 b运行ch 名字,我们也有自己的名字。我们将他们的 b运行ch 名称复制到我们的 remote-tracking 名称中,以便我们可以找到他们拥有但我们没有 b运行ch 的任何提交 名称,但在我们自己进行新提交之前,我们不需要任何 b运行ch 名称。