如何将本地存储库中的分支合并到另一个本地存储库中的分支
How can I merge a branch in a local repository to a branch that is in another local repository
假设我有 2 个本地存储库。
Local Repository A //Cloned from production remote repository.
Local Repository B //Cloned from development remote repository.
我从本地存储库 A 创建了自己的功能分支
FeatureA //In Local Repository A
从本地存储库A完成FeatureA后,我想将这个FeatureA分支合并到一个名为
的分支中
Developer //In Local Repository B
注意,FeatureA 和 Developer 这两个分支如何位于不同的本地存储库中。
如何将 FeatureA 分支合并到 Developer 分支?
Git实际上并没有合并b运行ches。事实上,就像 Git 中的大多数事情一样,b运行ches 在这里根本不重要:只有 commits 事情。 Git 合并 提交 .
这对您来说意味着,只有当所有 提交 相关时,您才能获得正确的结果。 (他们可能是,但我们看不到。你 可以找到。)
长
关于提交有一些重要的事情需要了解(这不是一个完整的列表,但这里对于合并很重要):
- 它们是通过哈希 ID 找到的。
- 散列 ID 大、难看且 random-looking(实际上 运行dom,但非常不可预测,人类无法使用)。
- 提交通过哈希 ID 引用其他较早的提交。这对于 提交本身 来说很好,毕竟它们是由计算机程序(或一系列计算机程序:我们称这些程序为
git
)读取的,但对倒霉的人应该负责这些程序和计算机。
由于这些特殊的要点,Git 对人类做出了很大的让步:它将让我们使用 b运行ch names 来查找提交.不仅如此,它还有一个专为人类设计的特殊功能。当我们:
- 首先将 Git 指向某个特定的 b运行ch,然后
- 直接 Git 进行 new 提交,
Git 将 更新我们“在” 上的 b运行ch 名称,以便该名称现在指的是 new commit,而不是指它刚刚表示的任何特定提交。
对于普通的,single-parent commit,我们可以画出这样的情况。我们首先用 single-letter pseudo-IDs 替换真实提交的真实哈希 ID,single-letter pseudo-IDs 我们 选择用我们脆弱的人类大脑工作。然后我们用一个箭头从它出来绘制每个提交,向后指向一个较早的提交。这将 latest 提交放在右边:
... <-F <-G <-H
所以这里 H
代表 最新 提交的哈希 ID。在内部表示 of 提交 H
、Git 的某处已经保存了早期提交 G
的真实哈希 ID。我们将其绘制为从 H
的表示中出来的箭头,指向 G
.
的表示
当然,G
本身也是一个commit,所以它也存储了一个previous-commit哈希ID:G
指向F
。 F
同样指向一些较早的提交,依此类推。这将永远重复,或者更确切地说,直到我们回到有史以来的第一次提交:在此公式中提交 A
。 (因此我们的存储库只有微不足道的八次提交。)
出于 Whosebug 的目的,由于惰性 and/or 字体问题,我倾向于停止绘制 commit-to-commit 箭头 作为 箭头,而是这样做:
A--B--...--G--H
但实际上从提交到提交的每个连接都只有一种方式:从 之后的 提交,例如 H
,到更早的提交。这是因为提交一旦完成,就完全是 100% read-only。 in 提交不能更改。1
当我们给这些图加上b运行ch的名字时,它们的工作原理就变得更加清晰了。假设我们有两个名字,main
和 develop
,*两个名字都指向提交 H
,像这样:
...--G--H <-- main, develop
这意味着包括 H
在内的所有提交都在 both b运行ches 上。我们现在必须选择 one b运行ch 为“开”,使用 git checkout
或 git switch
:3
git switch main
为了记住我们使用的是哪个 b运行ch,我们添加特殊名称 HEAD
,像这样全部大写,仅附加到其中一个 b运行通道名称:
...--G--H <-- main (HEAD), develop
这表明我们正在使用提交H
通过名称main
.
结帐或切换命令的工作方式(非常粗略):
- 从工作区中移除——Git 调用 工作树——从某些 other 提交,如果/根据需要;
- 用我们刚刚切换 到 .
的提交中的所有文件填充此工作区
我们马上就会看到这个,但现在,让我们切换到 develop
,或者甚至创建一个新的 b运行ch 名称 feature
。
1需要此 read-only 属性 才能使散列方案起作用。 提交的哈希 ID 只是存储在该提交中的所有位的加密校验和。如果您从 Git 数据库中取出一个提交,将其转换为普通数据,然后以某种方式修改该数据并将其放回 Git 数据库,您得到的是 新和d提交 具有 新的和不同的哈希 ID。旧提交在旧 ID 下保持不变。
Git 验证,在 object-extraction 时间,来自数据库 out 的所有位仍然校验和为原始值。如果他们不这样做,Git 宣布数据库损坏,并停止运行。由于文件内容也使用相同的散列技巧存储,因此我们可以确保 none 的文件曾被损坏。一旦它们 进入 存储库,它们将永远存在 2 并且永远无法更改。
2从技术上讲,可以从存储库数据库中删除提交,但这很棘手,我们不会在此处介绍。
3这里checkout
和switch
的用法没有区别。通过将一个命令 checkout
拆分为两个单独的命令 switch
和 restore
,最终清除了 git checkout
的某些历史错误,并且学习新的命令是有意义的只要您不被迫使用缺少新版本的 Git 的旧版本。 (虽然我现在已经使用 Git 超过 15 年了,所以我在这里有一些老习惯,有时 not-so-good 习惯。如果我使用 git checkout
,那是习惯,或者因为有人给了我可能要更新 Git 1.7 版本。)
在 b运行ch
上进行新提交
如果我们现在切换到现有的 b运行ch develop
,我们得到:
...--G--H <-- main, develop (HEAD)
为此,Git 需要删除来自提交 H
的所有文件,而是放入来自提交 H
的所有文件。这种remove-and-replace-with-sameness显然是愚蠢的,所以Git 针对这种特殊情况跳过这一步。4 Git 这次 根本不删除或替换任何文件。因此,如果我们开始进行更改但忘记切换到不同的 b运行ch(或创建一个新的 b运行ch;见下文),通常在您发现错误后立即进行更改是安全的。
无论如何,既然我们在 develop
,让我们以通常的方式进行新的提交。我会跳过很多重要的细节——特别是,我不会提到 Git 的 index 又名 staging area——并且假设你知道这里要知道的一切;5 但是,我们这样做了,我们现在有 Git 进行新的提交,我们将其称为 I
.:
I
/
...--G--H
新提交 I
指向现有提交 H
。但是现在特殊的魔术发生了:Git 将新提交的哈希 ID 写入当前 b运行ch name,即具有 HEAD
附上。所以为了完成我们的绘图——看看为什么我把 I
单独放在一行上——我们画这个:
I <-- develop (HEAD)
/
...--G--H <-- main
请注意名称 develop
现在如何指向 new 提交。所有其他 b运行ch 名称均未更改:只有名称 develop
已移动。
如果我们进行第二次新提交 J
,我们得到:
I--J <-- develop (HEAD)
/
...--G--H <-- main
通过 H
的提交都在 both b运行ches,而新提交 I-J
仅在 develop
.
如果我们现在 运行 git checkout main
或 git switch main
,我们得到:
I--J <-- develop
/
...--G--H <-- main (HEAD)
这一次,Git 确实必须删除一些文件——那些特定于提交 J
的文件——并用正确的文件替换它们以提交 H
。所以 Git 这样做了,如果我们现在检查我们的文件,我们会看到我们有来自提交 H
.6
的文件
现在我们通过名称 main
重新提交 H
,让我们创建一个 new b运行ch 名称 。我们必须为这个新名称选择一些提交到 point-to,通常的选择是“我们现在正在进行的提交”,即提交 H
:
git switch -c feature # or git checkout -b feature
这使我们处于这种状态:
I--J <-- develop
/
...--G--H <-- feature (HEAD), main
如果我们现在进行两次 更多 提交,我们得到:
I--J <-- develop
/
...--G--H <-- main
\
K--L <-- feature (HEAD)
我们现在处于合并有意义的状态。
4这种跳过的实现方式比这里描述的更聪明,但是对于“不切换提交的切换 b运行ches”,效果是始终允许更改。在更复杂的情况下,你会得到奇怪的效果;请参阅 Checkout another branch when there are uncommitted changes on the current branch 了解详细信息。
5有很多东西要知道!有关详细信息,请参阅其他答案或 Git 教程。
6这是脚注 4 中提到的复杂情况的来源。我们忘记提交的文件,或故意没有提交,可以在Git的索引又名staging-area中的工作树and/or中进行。但是,如果我们确保在 I
和 J
中提交所有内容,我们将处于“干净”状态,如 git status
所报告的那样,在结帐之前和之后——除非事情得到确实复杂,但我们不去那里。
合并就是合并工作
让我们再画一遍,但改变一些名字,切换运行四处走动,完全删除名称 main
(它挡住了路):
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
我们现在通过名称 br1
使用提交 J
。我们可以 运行:
git merge <hash-of-L>
或:
git merge br2
让 Git 找到提交 L
并进行合并工作。或者,如果我们还没有准备好合并提交 L
,但想合并提交 K
,我们可以 运行:
git merge <hash-of-K>
也就是说,我们会 运行 git log br2
并查看提交 L
,然后查看提交 K
。它有一些丑陋的大哈希 ID,b789abc...
或其他什么,我们用鼠标抓住它,cut-and-paste 样式,并生成如下命令:
git merge b789abc
(缩写哈希 ID 也有效,因此您可以重新输入前 4、7 或 15 个字符并停止,但这里很容易出错:为此我总是使用 cut-and-paste)。
我们通常不会费心像这样合并一定数量的提交,但在一些复杂的情况下——例如,如果我们有:
o--o--...--o <-- br1 (HEAD)
/
...--o--*
\
o--...--(thousands of commits)--...--o <-- br2
我们可能希望将合并分成更小的块,在 br2
的很长的一行中的某处选择一些提交首先合并:
o--...--o---M <-- br1 (HEAD)
/ /
...--o--* /
\ /
o--...--o--(hundreds of commits)--...--o <-- br2
合并了 500 个提交,我们将原来的 1400 个提交减少到 900 个要合并;我们可以再做 500 个,只剩下 400 个要合并,等等
在任何情况下,无论 many 如何提交,我们正在合并,合并操作的工作方式相同。鉴于:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
和git merge br2
、Git:
- 找到当前提交
J
(很简单:Git使用HEAD
);
- 找到另一个提交
L
(这很简单:Git 使用我们给它的 br2
);和
- 找到合并基础提交,提交
H
:这更难。
Git发现通过an algorithm合并基础提交,但我们可以将at描述为最佳共同祖先,在这种情况下,只需提交 H
.7
Git 现在在每个提交中使用快照——我们没有正确描述这一点,但每个提交都包含每个文件的完整快照——来弄清楚“我们的 b运行ch" br2
,通过比较提交 H
与提交 J
,并通过比较找出“他们的 b运行ch” 上的“他们改变了什么”提交 H
与提交 L
。这里对 diff 的三个提交是:
- 合并基础,在两个
git diff
命令的左边;
- 我们当前的或
HEAD
提交,在“我们的”差异的右侧;
- 我们选择合并的提交,在“他们的”差异的右侧。
两个差异的输出决定了要合并的更改集。
合并算法现在组合这些更改,将组合更改应用于合并基础提交中的快照——提交H
在这里——因此 保留我们的更改 而 添加他们的更改 ,这就是我们想要的。
成功合并这两组更改并将它们应用到合并基础后,Git 现在根据结果进行新提交。8 新提交是一个 merge commit,它在一个方面很特别:它不是指向单个父节点,而是指向 两个 父节点,就像这样:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
新合并提交 M
的 first 父级是我们在 运行 git merge
时使用的提交,即提交 J
。 other 父级是我们在命令行中命名的提交,在本例中为提交 L
。合并提交 M
作为所有文件的快照,具有 combining-and-applying-to-H
's-snapshot.
的结果
7正如维基百科文章所指出的,DAG 中不一定有一个唯一的 LCA 节点。对于这些情况,合并算法变得更加棘手;我们不会在这里介绍这些。
8如果Git合并变化失败,故意中途停止合并,留下我们清理一团糟。我们也不会在这里介绍这种情况。
这对您意味着什么
为了 git merge
完成它的工作,你给它的提交必须:
- 所有都在一个存储库中;和
- 相关,就最佳共享提交而言:
H
在我们上面的示例中。
当您只有一个存储库时,第一个条件很容易满足——所有提交都在 the(单个)存储库中——第二个通常是这种情况,因为我们通常通过从一些现有的 b运行ch 上的某个起点增长它来制作 new b运行ch。该共享 起点 提交是 a 共同起点,因此 a 共享提交。如果从那以后有合并,可能会有一些更好共享ommit,但除此之外这是 the 共享提交:
...--*--*--*--o--o <-- br1 (HEAD)
\
o----o <-- br2
加星标的提交 *
在两个 b运行 上,因此最右边的提交作为合并基础。或者:
...--*--*--*--o--o--M1--o--o <-- br1 (HEAD)
\ /
*----*----o----o <-- br2
同样,所有加星标的提交都在两个 b运行ches 上。合并提交 M1
的额外父级将 br2
加入 br1
,所以再一次,最右边的星号提交作为合并基础。一旦我们合并 M2
我们有:
...--*--*--*--o--o--M1--o--o--M2 <-- br1 (HEAD)
\ / /
*----*----*----* <-- br2
请注意如何合并“添加”所有其他 b运行ch 的提交到 br1
。
当你有两个独立的存储库时,提交是否相关?现在我们进入任何分布式版本控制系统的复杂性之一,例如 Git.
当您克隆一个Git存储库时,您实际上是在复制提交。某个存储库 R 的 git clone
制作了一些克隆 C,但是 C 拥有所有来自 R.9 的提交 在 Git 中,克隆会复制提交,但 不会复制 b运行ches,10 这在某种意义上很奇怪——例如,Mercurial 也复制了 b运行ches——但对你来说重要的是情况是 commits 被复制了。
现在,在 提交被复制到 C 后,有人可以在 R[=609] 中进行更多提交=], and/or 其他人可以在 C 中进行更多提交。但是,如果它们都遵循相同的标准程序——从它们的提交开始,然后仅仅添加——这些提交将全部“加入过去”,恰好与我们使用 单个 存储库的方式相同。
在这种情况下,您所要做的就是:
- 克隆 R 或 C 到 third Git 存储库, 那么
- 添加到该存储库 这两个存储库的 other 中的所有提交,这些提交还没有在你的第三个存储库中克隆。
第二步——“添加我们没有的提交”——似乎是一件大事。在某些方面,它是……但是我们已经因为克隆而不得不这么做了。也就是说,假设 Rcentral everyone 克隆了一些“真实来源”存储库。你让你的克隆 Cyou。 Alice 克隆 Calice,Bob 克隆 Cbob,等等。
在某些时候,某人 进行了新的提交,并最终——以某种方式——将他们的提交放入 Rcentral 。现在 每个拥有克隆 的人都必须将 那些 新提交到他们的克隆中,如果他们想看到并使用它们的话。所以我们有 git fetch
.
我们运行git获取<em>name</em>
。 Git 调用我们这里使用的 name
remote。当您克隆 Rcentral 时,您的 Git 在您的克隆 C 中添加一个标准远程名称,origin
。你的Git将Rcentral的URL存储在这个标准名称下,从现在开始,你可以运行:
git fetch origin
让你的 Git 调用 Rcentral Git。他们将列出他们的提交(通过哈希 ID)和他们的 b运行ch 名称(通过名称),并且您的 Git 将确定这些提交中是否有任何提交对您来说是 新的,如果有,就去获取。然后,您的 Git 将通过获取他们的 b运行ch 名称来设置 remote-tracking 名称,main
和 feature
等等,并在它们前面加上 origin/
:origin
部分来自 remote。这些名称“track”11 b运行ch names over on origin
, 所以它们是 remote-tracking origin
.
的名称
您可以添加更多 Git 存储库作为额外的远程。 也就是说,使用:
git remote add repo-xyz <url>
您添加第二个遥控器,使用名称 repo-xyz
,以存储给定的 url
。现在你可以 运行:
git fetch repo-xyz
你的Git会在你刚刚保存的URL调用Git,询问他们他们的b运行ch 名称和提交哈希 ID,并带来他们拥有的任何提交,而你没有。然后,您的 Git 将在您的克隆 C 中创建 remote-tracking 形式的名称 repo-xyz/*
。如果他们有 main
,您将有一个 repo-xyz/main
。如果他们有一个develop
,你就会有一个repo-xyz/develop
。
这些 remote-tracking 名称中的每一个都将准确记住一个提交哈希 ID,就像 C 中的每个 b运行ch 名称一样、Rcentral,或者这个添加的远程只记住一个提交哈希 ID。因为 git fetch
读取了它们的 current 状态,哟r remote-tracking 名字现在会记住他们 b运行ch 状态,因为你 运行 git fetch
.
所以,有了 运行 git fetch origin
和 git fetch repo-xyz
,你现在有:
- 你所有的提交,加上
- 所有提交
origin
都有你没有的,加上
- 所有提交
repo-xyz
有你没有的,加上
- remote-tracking 名称
origin/*
和 repo-xyz/*
以记住 b运行ch 名称并提交来自 origin
和 repo-xyz
的哈希 ID。
定位特定提交的 Remote-tracking 名称与定位特定提交的 b运行ch 名称一样有效。因此,您可以将 remote-tracking 名称传递给 git merge
。他们 不 在这里工作的唯一一件事是你不能“使用” remote-tracking 名称。那是因为它不是 b运行ch 名称,Git 只会让你将 HEAD
附加到 b运行ch 名称。如果你想要一个 b运行ch 名称指向某个现有 remote-tracking 名称所在的提交,你可以使用 git checkout
或 git switch
来执行此操作:
git switch -c update-some-abc-branch repo-xyz/abc-branch
因为你有两个遥控器(origin
和repo-xyz
),你可能会运行陷入你无法制造的烦恼一个名称,例如 main
,您在使用 both origin/main
and repo-xyz/main
时使用的名称。您可能需要使用一些时髦的不匹配的 b运行ch 名称,就像我在上面所做的那样。这很好用:不需要在每个存储库中使用 相同的 名称。12
这为您提供了您需要的所有信息:
- 在 您的 存储库 C 中创建 b运行ch 名称,以便在“打开”那些 b运行切;
- 运行
git merge
与 commits 由你的 b运行ch 名称识别 and/or 你的 remote-tracking名字。
只要你记得 Git 真正关心的是 提交和它们的哈希 ID,以及 你的 b运行ch 名称只是为了让你找到你选择的提交,你会没事的。
9制作省略某些提交的克隆是可能的,但我们会考虑我们 不 做的通常情况那。
10Git 使用 remote-tracking names。复制 b运行ch 名称是可能的,但会导致混淆。 remote-tracking 名称技术反而会导致……混乱。我不确定这里是否有那么大的改进。但就是这样。
11Git 严重滥用这个动词 track。不过,这又是事实。 Git 称这些东西为 remote-tracking b运行ch names,但它们实际上并不是 b运行ch 名称,所以我使用 remote-tracking 名称 因为它们是名称并且它们确实跟踪其他(远程)名称。
12这就是强制每个人都使用相同的 b运行ch 名称的 Mercurial 方法非常有用的地方。这也是强制每个人都使用相同的 b运行ch 名称的 Mercurial 方法非常有害的地方。这完全取决于您需要完成什么。
我发布这个答案以备将来参考。
我错误地认为存在两个不同的远程存储库(一个用于生产,一个用于开发)。
原来,这个项目只有一个远程仓库。
这是我应该做的。
从远程存储库,将其克隆到我的本地计算机中一次。
然后检查我本地机器上的 Production 分支。
从我刚刚签出的 Production 分支创建我自己的分支。
完成功能后,将我的分支合并到 Developer 分支。
假设我有 2 个本地存储库。
Local Repository A //Cloned from production remote repository.
Local Repository B //Cloned from development remote repository.
我从本地存储库 A 创建了自己的功能分支
FeatureA //In Local Repository A
从本地存储库A完成FeatureA后,我想将这个FeatureA分支合并到一个名为
的分支中Developer //In Local Repository B
注意,FeatureA 和 Developer 这两个分支如何位于不同的本地存储库中。
如何将 FeatureA 分支合并到 Developer 分支?
Git实际上并没有合并b运行ches。事实上,就像 Git 中的大多数事情一样,b运行ches 在这里根本不重要:只有 commits 事情。 Git 合并 提交 .
这对您来说意味着,只有当所有 提交 相关时,您才能获得正确的结果。 (他们可能是,但我们看不到。你 可以找到。)
长
关于提交有一些重要的事情需要了解(这不是一个完整的列表,但这里对于合并很重要):
- 它们是通过哈希 ID 找到的。
- 散列 ID 大、难看且 random-looking(实际上 运行dom,但非常不可预测,人类无法使用)。
- 提交通过哈希 ID 引用其他较早的提交。这对于 提交本身 来说很好,毕竟它们是由计算机程序(或一系列计算机程序:我们称这些程序为
git
)读取的,但对倒霉的人应该负责这些程序和计算机。
由于这些特殊的要点,Git 对人类做出了很大的让步:它将让我们使用 b运行ch names 来查找提交.不仅如此,它还有一个专为人类设计的特殊功能。当我们:
- 首先将 Git 指向某个特定的 b运行ch,然后
- 直接 Git 进行 new 提交,
Git 将 更新我们“在” 上的 b运行ch 名称,以便该名称现在指的是 new commit,而不是指它刚刚表示的任何特定提交。
对于普通的,single-parent commit,我们可以画出这样的情况。我们首先用 single-letter pseudo-IDs 替换真实提交的真实哈希 ID,single-letter pseudo-IDs 我们 选择用我们脆弱的人类大脑工作。然后我们用一个箭头从它出来绘制每个提交,向后指向一个较早的提交。这将 latest 提交放在右边:
... <-F <-G <-H
所以这里 H
代表 最新 提交的哈希 ID。在内部表示 of 提交 H
、Git 的某处已经保存了早期提交 G
的真实哈希 ID。我们将其绘制为从 H
的表示中出来的箭头,指向 G
.
当然,G
本身也是一个commit,所以它也存储了一个previous-commit哈希ID:G
指向F
。 F
同样指向一些较早的提交,依此类推。这将永远重复,或者更确切地说,直到我们回到有史以来的第一次提交:在此公式中提交 A
。 (因此我们的存储库只有微不足道的八次提交。)
出于 Whosebug 的目的,由于惰性 and/or 字体问题,我倾向于停止绘制 commit-to-commit 箭头 作为 箭头,而是这样做:
A--B--...--G--H
但实际上从提交到提交的每个连接都只有一种方式:从 之后的 提交,例如 H
,到更早的提交。这是因为提交一旦完成,就完全是 100% read-only。 in 提交不能更改。1
当我们给这些图加上b运行ch的名字时,它们的工作原理就变得更加清晰了。假设我们有两个名字,main
和 develop
,*两个名字都指向提交 H
,像这样:
...--G--H <-- main, develop
这意味着包括 H
在内的所有提交都在 both b运行ches 上。我们现在必须选择 one b运行ch 为“开”,使用 git checkout
或 git switch
:3
git switch main
为了记住我们使用的是哪个 b运行ch,我们添加特殊名称 HEAD
,像这样全部大写,仅附加到其中一个 b运行通道名称:
...--G--H <-- main (HEAD), develop
这表明我们正在使用提交H
通过名称main
.
结帐或切换命令的工作方式(非常粗略):
- 从工作区中移除——Git 调用 工作树——从某些 other 提交,如果/根据需要;
- 用我们刚刚切换 到 . 的提交中的所有文件填充此工作区
我们马上就会看到这个,但现在,让我们切换到 develop
,或者甚至创建一个新的 b运行ch 名称 feature
。
1需要此 read-only 属性 才能使散列方案起作用。 提交的哈希 ID 只是存储在该提交中的所有位的加密校验和。如果您从 Git 数据库中取出一个提交,将其转换为普通数据,然后以某种方式修改该数据并将其放回 Git 数据库,您得到的是 新和d提交 具有 新的和不同的哈希 ID。旧提交在旧 ID 下保持不变。
Git 验证,在 object-extraction 时间,来自数据库 out 的所有位仍然校验和为原始值。如果他们不这样做,Git 宣布数据库损坏,并停止运行。由于文件内容也使用相同的散列技巧存储,因此我们可以确保 none 的文件曾被损坏。一旦它们 进入 存储库,它们将永远存在 2 并且永远无法更改。
2从技术上讲,可以从存储库数据库中删除提交,但这很棘手,我们不会在此处介绍。
3这里checkout
和switch
的用法没有区别。通过将一个命令 checkout
拆分为两个单独的命令 switch
和 restore
,最终清除了 git checkout
的某些历史错误,并且学习新的命令是有意义的只要您不被迫使用缺少新版本的 Git 的旧版本。 (虽然我现在已经使用 Git 超过 15 年了,所以我在这里有一些老习惯,有时 not-so-good 习惯。如果我使用 git checkout
,那是习惯,或者因为有人给了我可能要更新 Git 1.7 版本。)
在 b运行ch
上进行新提交如果我们现在切换到现有的 b运行ch develop
,我们得到:
...--G--H <-- main, develop (HEAD)
为此,Git 需要删除来自提交 H
的所有文件,而是放入来自提交 H
的所有文件。这种remove-and-replace-with-sameness显然是愚蠢的,所以Git 针对这种特殊情况跳过这一步。4 Git 这次 根本不删除或替换任何文件。因此,如果我们开始进行更改但忘记切换到不同的 b运行ch(或创建一个新的 b运行ch;见下文),通常在您发现错误后立即进行更改是安全的。
无论如何,既然我们在 develop
,让我们以通常的方式进行新的提交。我会跳过很多重要的细节——特别是,我不会提到 Git 的 index 又名 staging area——并且假设你知道这里要知道的一切;5 但是,我们这样做了,我们现在有 Git 进行新的提交,我们将其称为 I
.:
I
/
...--G--H
新提交 I
指向现有提交 H
。但是现在特殊的魔术发生了:Git 将新提交的哈希 ID 写入当前 b运行ch name,即具有 HEAD
附上。所以为了完成我们的绘图——看看为什么我把 I
单独放在一行上——我们画这个:
I <-- develop (HEAD)
/
...--G--H <-- main
请注意名称 develop
现在如何指向 new 提交。所有其他 b运行ch 名称均未更改:只有名称 develop
已移动。
如果我们进行第二次新提交 J
,我们得到:
I--J <-- develop (HEAD)
/
...--G--H <-- main
通过 H
的提交都在 both b运行ches,而新提交 I-J
仅在 develop
.
如果我们现在 运行 git checkout main
或 git switch main
,我们得到:
I--J <-- develop
/
...--G--H <-- main (HEAD)
这一次,Git 确实必须删除一些文件——那些特定于提交 J
的文件——并用正确的文件替换它们以提交 H
。所以 Git 这样做了,如果我们现在检查我们的文件,我们会看到我们有来自提交 H
.6
现在我们通过名称 main
重新提交 H
,让我们创建一个 new b运行ch 名称 。我们必须为这个新名称选择一些提交到 point-to,通常的选择是“我们现在正在进行的提交”,即提交 H
:
git switch -c feature # or git checkout -b feature
这使我们处于这种状态:
I--J <-- develop
/
...--G--H <-- feature (HEAD), main
如果我们现在进行两次 更多 提交,我们得到:
I--J <-- develop
/
...--G--H <-- main
\
K--L <-- feature (HEAD)
我们现在处于合并有意义的状态。
4这种跳过的实现方式比这里描述的更聪明,但是对于“不切换提交的切换 b运行ches”,效果是始终允许更改。在更复杂的情况下,你会得到奇怪的效果;请参阅 Checkout another branch when there are uncommitted changes on the current branch 了解详细信息。
5有很多东西要知道!有关详细信息,请参阅其他答案或 Git 教程。
6这是脚注 4 中提到的复杂情况的来源。我们忘记提交的文件,或故意没有提交,可以在Git的索引又名staging-area中的工作树and/or中进行。但是,如果我们确保在 I
和 J
中提交所有内容,我们将处于“干净”状态,如 git status
所报告的那样,在结帐之前和之后——除非事情得到确实复杂,但我们不去那里。
合并就是合并工作
让我们再画一遍,但改变一些名字,切换运行四处走动,完全删除名称 main
(它挡住了路):
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
我们现在通过名称 br1
使用提交 J
。我们可以 运行:
git merge <hash-of-L>
或:
git merge br2
让 Git 找到提交 L
并进行合并工作。或者,如果我们还没有准备好合并提交 L
,但想合并提交 K
,我们可以 运行:
git merge <hash-of-K>
也就是说,我们会 运行 git log br2
并查看提交 L
,然后查看提交 K
。它有一些丑陋的大哈希 ID,b789abc...
或其他什么,我们用鼠标抓住它,cut-and-paste 样式,并生成如下命令:
git merge b789abc
(缩写哈希 ID 也有效,因此您可以重新输入前 4、7 或 15 个字符并停止,但这里很容易出错:为此我总是使用 cut-and-paste)。
我们通常不会费心像这样合并一定数量的提交,但在一些复杂的情况下——例如,如果我们有:
o--o--...--o <-- br1 (HEAD)
/
...--o--*
\
o--...--(thousands of commits)--...--o <-- br2
我们可能希望将合并分成更小的块,在 br2
的很长的一行中的某处选择一些提交首先合并:
o--...--o---M <-- br1 (HEAD)
/ /
...--o--* /
\ /
o--...--o--(hundreds of commits)--...--o <-- br2
合并了 500 个提交,我们将原来的 1400 个提交减少到 900 个要合并;我们可以再做 500 个,只剩下 400 个要合并,等等
在任何情况下,无论 many 如何提交,我们正在合并,合并操作的工作方式相同。鉴于:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
和git merge br2
、Git:
- 找到当前提交
J
(很简单:Git使用HEAD
); - 找到另一个提交
L
(这很简单:Git 使用我们给它的br2
);和 - 找到合并基础提交,提交
H
:这更难。
Git发现通过an algorithm合并基础提交,但我们可以将at描述为最佳共同祖先,在这种情况下,只需提交 H
.7
Git 现在在每个提交中使用快照——我们没有正确描述这一点,但每个提交都包含每个文件的完整快照——来弄清楚“我们的 b运行ch" br2
,通过比较提交 H
与提交 J
,并通过比较找出“他们的 b运行ch” 上的“他们改变了什么”提交 H
与提交 L
。这里对 diff 的三个提交是:
- 合并基础,在两个
git diff
命令的左边; - 我们当前的或
HEAD
提交,在“我们的”差异的右侧; - 我们选择合并的提交,在“他们的”差异的右侧。
两个差异的输出决定了要合并的更改集。
合并算法现在组合这些更改,将组合更改应用于合并基础提交中的快照——提交H
在这里——因此 保留我们的更改 而 添加他们的更改 ,这就是我们想要的。
成功合并这两组更改并将它们应用到合并基础后,Git 现在根据结果进行新提交。8 新提交是一个 merge commit,它在一个方面很特别:它不是指向单个父节点,而是指向 两个 父节点,就像这样:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
新合并提交 M
的 first 父级是我们在 运行 git merge
时使用的提交,即提交 J
。 other 父级是我们在命令行中命名的提交,在本例中为提交 L
。合并提交 M
作为所有文件的快照,具有 combining-and-applying-to-H
's-snapshot.
7正如维基百科文章所指出的,DAG 中不一定有一个唯一的 LCA 节点。对于这些情况,合并算法变得更加棘手;我们不会在这里介绍这些。
8如果Git合并变化失败,故意中途停止合并,留下我们清理一团糟。我们也不会在这里介绍这种情况。
这对您意味着什么
为了 git merge
完成它的工作,你给它的提交必须:
- 所有都在一个存储库中;和
- 相关,就最佳共享提交而言:
H
在我们上面的示例中。
当您只有一个存储库时,第一个条件很容易满足——所有提交都在 the(单个)存储库中——第二个通常是这种情况,因为我们通常通过从一些现有的 b运行ch 上的某个起点增长它来制作 new b运行ch。该共享 起点 提交是 a 共同起点,因此 a 共享提交。如果从那以后有合并,可能会有一些更好共享ommit,但除此之外这是 the 共享提交:
...--*--*--*--o--o <-- br1 (HEAD)
\
o----o <-- br2
加星标的提交 *
在两个 b运行 上,因此最右边的提交作为合并基础。或者:
...--*--*--*--o--o--M1--o--o <-- br1 (HEAD)
\ /
*----*----o----o <-- br2
同样,所有加星标的提交都在两个 b运行ches 上。合并提交 M1
的额外父级将 br2
加入 br1
,所以再一次,最右边的星号提交作为合并基础。一旦我们合并 M2
我们有:
...--*--*--*--o--o--M1--o--o--M2 <-- br1 (HEAD)
\ / /
*----*----*----* <-- br2
请注意如何合并“添加”所有其他 b运行ch 的提交到 br1
。
当你有两个独立的存储库时,提交是否相关?现在我们进入任何分布式版本控制系统的复杂性之一,例如 Git.
当您克隆一个Git存储库时,您实际上是在复制提交。某个存储库 R 的 git clone
制作了一些克隆 C,但是 C 拥有所有来自 R.9 的提交 在 Git 中,克隆会复制提交,但 不会复制 b运行ches,10 这在某种意义上很奇怪——例如,Mercurial 也复制了 b运行ches——但对你来说重要的是情况是 commits 被复制了。
现在,在 提交被复制到 C 后,有人可以在 R[=609] 中进行更多提交=], and/or 其他人可以在 C 中进行更多提交。但是,如果它们都遵循相同的标准程序——从它们的提交开始,然后仅仅添加——这些提交将全部“加入过去”,恰好与我们使用 单个 存储库的方式相同。
在这种情况下,您所要做的就是:
- 克隆 R 或 C 到 third Git 存储库, 那么
- 添加到该存储库 这两个存储库的 other 中的所有提交,这些提交还没有在你的第三个存储库中克隆。
第二步——“添加我们没有的提交”——似乎是一件大事。在某些方面,它是……但是我们已经因为克隆而不得不这么做了。也就是说,假设 Rcentral everyone 克隆了一些“真实来源”存储库。你让你的克隆 Cyou。 Alice 克隆 Calice,Bob 克隆 Cbob,等等。
在某些时候,某人 进行了新的提交,并最终——以某种方式——将他们的提交放入 Rcentral 。现在 每个拥有克隆 的人都必须将 那些 新提交到他们的克隆中,如果他们想看到并使用它们的话。所以我们有 git fetch
.
我们运行git获取<em>name</em>
。 Git 调用我们这里使用的 name
remote。当您克隆 Rcentral 时,您的 Git 在您的克隆 C 中添加一个标准远程名称,origin
。你的Git将Rcentral的URL存储在这个标准名称下,从现在开始,你可以运行:
git fetch origin
让你的 Git 调用 Rcentral Git。他们将列出他们的提交(通过哈希 ID)和他们的 b运行ch 名称(通过名称),并且您的 Git 将确定这些提交中是否有任何提交对您来说是 新的,如果有,就去获取。然后,您的 Git 将通过获取他们的 b运行ch 名称来设置 remote-tracking 名称,main
和 feature
等等,并在它们前面加上 origin/
:origin
部分来自 remote。这些名称“track”11 b运行ch names over on origin
, 所以它们是 remote-tracking origin
.
您可以添加更多 Git 存储库作为额外的远程。 也就是说,使用:
git remote add repo-xyz <url>
您添加第二个遥控器,使用名称 repo-xyz
,以存储给定的 url
。现在你可以 运行:
git fetch repo-xyz
你的Git会在你刚刚保存的URL调用Git,询问他们他们的b运行ch 名称和提交哈希 ID,并带来他们拥有的任何提交,而你没有。然后,您的 Git 将在您的克隆 C 中创建 remote-tracking 形式的名称 repo-xyz/*
。如果他们有 main
,您将有一个 repo-xyz/main
。如果他们有一个develop
,你就会有一个repo-xyz/develop
。
这些 remote-tracking 名称中的每一个都将准确记住一个提交哈希 ID,就像 C 中的每个 b运行ch 名称一样、Rcentral,或者这个添加的远程只记住一个提交哈希 ID。因为 git fetch
读取了它们的 current 状态,哟r remote-tracking 名字现在会记住他们 b运行ch 状态,因为你 运行 git fetch
.
所以,有了 运行 git fetch origin
和 git fetch repo-xyz
,你现在有:
- 你所有的提交,加上
- 所有提交
origin
都有你没有的,加上 - 所有提交
repo-xyz
有你没有的,加上 - remote-tracking 名称
origin/*
和repo-xyz/*
以记住 b运行ch 名称并提交来自origin
和repo-xyz
的哈希 ID。
Remote-tracking 名称与定位特定提交的 b运行ch 名称一样有效。因此,您可以将 remote-tracking 名称传递给 git merge
。他们 不 在这里工作的唯一一件事是你不能“使用” remote-tracking 名称。那是因为它不是 b运行ch 名称,Git 只会让你将 HEAD
附加到 b运行ch 名称。如果你想要一个 b运行ch 名称指向某个现有 remote-tracking 名称所在的提交,你可以使用 git checkout
或 git switch
来执行此操作:
git switch -c update-some-abc-branch repo-xyz/abc-branch
因为你有两个遥控器(origin
和repo-xyz
),你可能会运行陷入你无法制造的烦恼一个名称,例如 main
,您在使用 both origin/main
and repo-xyz/main
时使用的名称。您可能需要使用一些时髦的不匹配的 b运行ch 名称,就像我在上面所做的那样。这很好用:不需要在每个存储库中使用 相同的 名称。12
这为您提供了您需要的所有信息:
- 在 您的 存储库 C 中创建 b运行ch 名称,以便在“打开”那些 b运行切;
- 运行
git merge
与 commits 由你的 b运行ch 名称识别 and/or 你的 remote-tracking名字。
只要你记得 Git 真正关心的是 提交和它们的哈希 ID,以及 你的 b运行ch 名称只是为了让你找到你选择的提交,你会没事的。
9制作省略某些提交的克隆是可能的,但我们会考虑我们 不 做的通常情况那。
10Git 使用 remote-tracking names。复制 b运行ch 名称是可能的,但会导致混淆。 remote-tracking 名称技术反而会导致……混乱。我不确定这里是否有那么大的改进。但就是这样。
11Git 严重滥用这个动词 track。不过,这又是事实。 Git 称这些东西为 remote-tracking b运行ch names,但它们实际上并不是 b运行ch 名称,所以我使用 remote-tracking 名称 因为它们是名称并且它们确实跟踪其他(远程)名称。
12这就是强制每个人都使用相同的 b运行ch 名称的 Mercurial 方法非常有用的地方。这也是强制每个人都使用相同的 b运行ch 名称的 Mercurial 方法非常有害的地方。这完全取决于您需要完成什么。
我发布这个答案以备将来参考。
我错误地认为存在两个不同的远程存储库(一个用于生产,一个用于开发)。
原来,这个项目只有一个远程仓库。
这是我应该做的。
从远程存储库,将其克隆到我的本地计算机中一次。
然后检查我本地机器上的 Production 分支。
从我刚刚签出的 Production 分支创建我自己的分支。
完成功能后,将我的分支合并到 Developer 分支。