是否有可能通过在推送之前拉出他的分支、变基和删除他的提交来搞乱同事的代码?
Is it possible to mess up a coworkers code by pulling his branch, rebasing, and deleting his commits before pushing?
我对 git 有点陌生,正在尝试了解 branches/rebase 的工作原理,如果有可能我搞砸了。
我和我的同事都在不同的分支机构,我需要他为我的工作所做的更改。所以我取消了他的更改。然后我做了
git rebase -i HEAD~3
并且刚刚删除了我不需要的他的提交。然后我向我们的回购推送并创建了一个拉取请求,一切看起来都不错。看起来就像我需要他的更改以及我自己的更改的一次提交。
我还没有合并。是否有可能一旦他合并,然后我合并,不知何故,因为我删除了他在我正在合并的分支上的提交,它删除了他的代码?
许多事情 可能 Git。要了解 实际上 与 Git 发生了什么,您必须了解 b运行ch 是什么以及为您做什么。这实际上很少,要理解 that,您必须了解 Git commit 是什么以及为您做什么。
在这种特殊情况下,我认为您和您的 co-worker 是安全的,但这实际上取决于你们如何使用各种 Git 存储库。请注意,您和他有 单独的 Git 存储库 。您可能还都使用存储在其他地方的 third 存储库,例如 GitHub。您没有共享 b运行ches,也没有共享各种存储库本身的任何其他内容,这主要是一件好事。这意味着您实际共享的唯一内容是 提交 .
这里提一下,b运行ch name主要是一种存储一个(单个)ID的方式commit。 b运行ch 名称也有很多辅助用途,但是存储提交哈希 ID 是任何 Git 的各种名称的本质——b运行ch 名称,标签名称和其他名称。这里有一个主题:一切都是关于提交。 Git 与 b运行ches 无关,尽管 b运行ch 名称可以帮助您 find 提交; Git 与文件无关,但提交 包含 文件。最后,一切都与提交有关。
什么是提交?
提交是一种存储一堆文件的快照的方式——我们称之为数据,或者通常称为树——以及关于提交的其他信息,我们称之为元数据。因此,提交是存储这两项的东西:源快照或树,以及有关提交的信息,例如提交人、时间和原因(他们的日志消息)。但是我们需要更仔细地看一下,看看每个提交里面有什么,以及我们如何找到一个提交。
每个提交都有一个唯一的编号。这个数字不是一个很好的简单顺序计数:我们没有提交 #1,然后是 #2、#3 等等。相反,该数字是一个丑陋的大 哈希 ID,以十六进制表示。它看起来 运行dom,但它不是。
任何一次提交的哈希 ID 实际上是该提交全部内容的加密校验和:数据和元数据。因为每个 Git 都以相同的方式计算这个 ID,所以两个 Git 可以通过比较哈希 ID 来判断一个是否有另一个的提交。 Git gua运行tees1 这个哈希 ID 是唯一的,除其他外,在元数据中为每个提交添加一个 date-and-time-stamp,因此,即使您保存相同的文件,使用相同的用户名和电子邮件地址等,除非您在相同的 time[=601= 保存文件,否则您将获得一个新的和不同的提交] 两次——如果你这样做了,你怎么知道你做了两次?
Git 通过哈希 ID 查找提交和其他内部 Git object 也使用此哈希方案的提交。实际上,Git 的大部分是一个简单的 key-value database,其键是散列 ID,内容是 object。2 这提供了一个data-integrity 校验:给定一个密钥,Git 查找数据,然后确保存储的数据仍然是密钥的校验和。
这样做的一个重要结果是每次提交后,都是 100% read-only。如果你把数据库的任何 object out 搞砸了,即使是一点点的改变,然后写回来,你得到的是一个新的和不同的 object 使用新密钥。3 因此,当您使用提交执行此操作时,您只需添加一个新提交。旧提交保留在数据库中。4
提交中的文件同样是 read-only,并且会永久存储(但请参阅脚注 4)。它们是压缩的 Git-only 格式,为了解决在每个快照中存储每个文件会使数据库增长得非常快的问题,它们都是自动 de-duplicated(见脚注 3)。因此,如果一千次提交重复使用相同版本的 README.md
,那么数据库中实际上只有一个副本。
关于提交的最后一件事——你只需要记住这些部分,真的——是每个提交在其元数据中存储一些前一个的哈希ID提交或提交。大多数提交只存储一个先前的提交哈希 ID。这是提交的 parent.
总结:
- 每个提交都由其哈希 ID 编号。
- 每个提交都存储数据——所有文件的快照,但压缩文件和de-duplicated——和元数据,包括一些先前提交的哈希 ID。
- 所有这些都是read-only。你不能直接看到这些东西,甚至不能直接使用提交中的文件,因为使用它们都是这种内部格式。
1pigeonhole principle tells us that this must eventually fail. The hash IDs are as big and ugly as they are to make sure that it won't accidentally fail in any sensible amount of time. It's possible to make this fail intentionally,所以Git正在从SHA-1移动到SHA-256,但同时这在实践中不是问题.
2提交是四种内部类型之一 object。为了完整起见,其他三个是 blob、tag(或带注释的标签)和 tree.
3如果数据与数据库中已有的数据匹配,Git 只是说 aha, a duplicate 而不是根本不用存储它。例如,这就是 Git de-duplicates 存储文件的方式。提交通过 time-stamps 和其他与之前提交不匹配的元数据获得唯一性。
4这意味着数据库只会增长。 是一种丢弃未使用的东西的方法,但我们不会在这里详细介绍。
B运行通道名称
“提交存储它们的 parent 哈希 ID”的一个主要结果是提交形成了一个 向后看的链 。也就是说,假设我们有一系列提交,并且我们使用单个大写字母代表哈希 ID。我们可以这样画:
... <-F <-G <-H
其中 H
是某个提交链中 last 提交的哈希 ID。提交 H
存储快照和元数据,在元数据中,H
存储早期提交 G
的哈希 ID。我们说H
指向G
,或者说G
是H
的parent。 G
也有快照和元数据,在 G
中我们将找到早期提交 F
的哈希 ID。 F
依次指向另一个更早的提交,依此类推。这一切一直持续到我们到达“第一个”提交,它没有 parent。所以我们只需要 H
的实际哈希 ID。从那里,我们可以找到 每个较早的提交 .
但是有一个问题:我们在哪里可以找到提交 H
的哈希 ID?我已经放弃了:它在 b运行ch name 中。这就是 b运行ch 名称的含义和作用:它包含某些提交链中 last 提交的哈希 ID。所以我们可以这样绘制提交:
..--G--H <-- branch
关于 Git b运行ch 名称的有趣之处在于它们 移动 。让我们创建一个新的 b运行ch 名称,例如 feature
,并让它也指向 H
:
..--G--H <-- branch, feature
现在我们需要知道使用哪个b运行ch名称,所以让我们将特殊名称HEAD
附加到一个b运行通道名称:
..--G--H <-- branch (HEAD), feature
这告诉我们,我们将使用 通过name branch
提交H
。如果我们 运行:
git checkout feature
我们将继续使用提交 H
,但是通过 name feature
:
..--G--H <-- branch, feature (HEAD)
假设我们现在进行 new 提交。不谈太多细节,我们只说 Git 为新提交分配了一个新的哈希 ID,我们将其称为 I
。 Git 会将 I
的 parent 设置为 H
,因此我们可以这样绘制:
..--G--H
\
I
现在我们看到了 Git 的特殊技巧:当 Git 进行了这个新提交时,Git 将其哈希 ID 写入 HEAD
附上:
..--G--H <-- branch
\
I <-- feature (HEAD)
这就是 b运行ch 增长的方式——好吧,一种方式:每次我们进行新的提交时,Git 都会更新 name 以指向到我们刚刚做出的新承诺。新提交指向刚才名称指向的位置。
T运行将提交提交到另一个存储库
我们在上面看到了 git commit
如何进行新提交的简要视图。 git merge
命令也可以像其他各种命令一样进行新的提交。完成这些新提交后,您可能希望 与其他 Git 用户共享 它们,例如您的 co-worker。为此,您必须让您的 Git 调用其他 Git。您将使用 URL 执行此操作:一些服务将回答 URL 并调用另一个 Git.
为了避免每次都输入长的 URL,Git 将把 URL 存储在像 origin
这样的短名称下。我们称这个简称为 remote。您可以使用 git fetch
调用远程,这意味着 从他们那里获取我的提交 ,或 git push
,这意味着 让我发送提交给他们。这些操作非常相似,当然方向相反,但最后它们也完全不同。两者都是通过让您的 Git 调用他们的 Git 开始的,之后他们的 Git 可能会列出 他们的 b运行ch 名称,以及与这些名称一起使用的哈希 ID。5 现在您的 Git 知道他们的 Git 有什么——b运行ch 名称和哈希 ID —实际 t运行sfer 开始:
对于 git fetch
,此列表构成一个产品:您可以提交这些内容。您的 Git 开始如果您还没有那些特定的提交并且您想要 b运行ch,请通过他们的哈希 ID 向他们的 Git 询问特定的提交。如果您的 Git 要求一些提交,他们的 Git 现在必须提供该提交的 parent 或 parents,并且再次,你的 Git 要么说 请发送那个提交 要么 不,谢谢,我已经有了那个 .
使用 git push
,您的 Git 提供 last 提交,通过哈希 ID,来自您的 b运行ches(假设您使用 git push origin <em>someb运行ch</em>
,即)。他们的Git要么说请发送,要么说不,谢谢,就像上面一样。如果需要,这反过来会导致您的 Git 提供其 parent,依此类推。
现在发送方知道要发送什么以及接收方已经拥有什么,发送方打包要发送的提交以及接收方没有的任何文件。通过了解接收方 确实 的提交——发送方也有相同的提交,它们具有相同的文件——发送方可以发送 slimmed-down 包。6 接收方修复了该包裹以根据需要填充任何缺失的部分(详细信息远远超出了本答案的范围)。
最后,无论谁是接收者都必须设置一些名字或名字来记住last 在每个 b运行ch 中提交。在这里,fetch和push又分道扬镳了。
使用 git fetch
,当您的 Git 有他们的 b运行ch 提示,并且知道他们的 b运行ch names,你的 Git 所做的是 重命名 他们的兄弟 运行 。他们的兄弟运行是他们的,而不是你的,如果他们更新了他们的 develop
,那不会例如,这并不意味着您希望自己的 develop
b运行ch 名称从 您的 新提交中删除。因此,如果他们有 develop
,您的 Git 会创建或更新您的 origin/develop
,而不是您的 develop
。这假设您正在执行 git fetch origin
。如果您为遥控器使用其他名称,例如 bob
,您将得到 bob/develop
,例如。
与 git push
另一方面,您的 Git 要求他们的 Git 设置他们的一个 b运行ch 名称。这需要特别小心;我们将在下面看到更多相关信息。
5这一步已经成为一些long-lived存储库的痛点,这些存储库有很多b运行ches和标签,更新的Git 协议允许 server-side 过滤名称,以避免在此处列出完整的名称。否则,您可能会花 10 分钟来获取 name-and-hash-ID 信息,最后却决定,您的 Git 只需要一次提交,而 t运行 会在几秒钟内完成。但是原理还是一样的
6从技术上讲,发件人通常会在这里构建 Git 所谓的 thin pack。有些协议根本不支持包:发送方必须发送单独的 objects,这要慢得多。不过,现代 t运行sfer 协议都使用精简包。
既然我们知道了这么多,让我们观察一个git fetch
的动作
假设您的存储库中有这个:
I--J <-- your-feature (HEAD)
/
...--G--H <-- master, origin/master
与此同时,您的 co-worker 在他的存储库中有这个:
...--G--H <-- master, origin/master
\
K--L <-- his-feature (HEAD)
请记住,每个哈希 ID 都是唯一的,因此只有 shared...--G--H
提交在 both 存储库中。
他现在 运行s git push origin his-feature
,将他的提交 K-L
发送到 GitHub 存储库,并创建一个新的 b运行ch 名称那里,his-feature
,所以 GitHub 上的第三个存储库有这个:
...--G--H <-- master
\
K--L <-- his-feature
请注意,我们不知道或不关心 GitHub 存储库在哪个 b运行ch 上(它实际上可能在其 master
上,但它是 bare 存储库,这并不意味着什么)。 GitHub 存储库没有 origin/*
名称,因为它没有从其他存储库复制名称:GitHub 上没有人以管理员身份访问该存储库,运行s git fetch
.
如果您现在 运行 git fetch origin
,您的 Git 联系 GitHub Git,它列出了它的 b运行ch 名称及其相应的提交:master
,即提交H
,和his-feature
,即提交L
。你的 Git 已经有 H
,所以它不需要任何东西,但是你的 Git 要求 L
然后 K
(然后又一次有H
已经)。因此,您将 K-L
添加到您的存储库,然后 您的 Git 创建您自己的 origin/his-branch
。您现在拥有:
I--J <-- your-feature (HEAD)
/
...--G--H <-- master, origin/master
\
K--L <-- origin/his-feature
请注意,您正在处理或使用的任何内容都没有改变。您拥有与之前相同的所有提交,加上 两个新提交。您的 b运行ch 名称中的 None 已更改,但您已获得此 origin/his-feature
名称。7
7如果你用的是很旧的Git 的版本早于 1.8.4,并且您使用 git pull
而不是 git fetch
,您不会获得 origin/*
名称。在这种情况下,我建议避免使用 git pull
或升级 Git 版本。这并不重要,但在 Git 版本中落后那么多充其量是令人讨厌的:从那以后你错过了很多改进。
git pull
表示运行git fetch
,然后运行第二个Git命令
您之前提到过您:
pulled his changes
我假设你的意思是你 运行 git pull
使用 b运行ch 名称 his-feature
。我个人不喜欢 git pull
命令,因为它非常棘手:它首先 运行s git fetch
,这更直接,但然后立即 运行s 第二 Git命令。你必须选择 Git 命令到 运行 之前 你有机会看到 git fetch
实际上 做了什么.但是在 git fetch
之后,你经常想使用 git merge
,这是默认的第二个命令。因此,让两个命令 运行 一个紧接着另一个是很方便的——好吧,有时,或者很多时候。这就是 git pull
为您所做的。所以现在是时候看看 git merge
.
但是请注意,您并没有真正提取他的更改:相反,您获取了他的提交——提交保存快照,而不是变化——然后合并。这是处理更改的 merge 步骤。说 pull his changes 有点草率。在平时的谈话中还好,但是当你想到你在做什么,并把它画出来以确保一切正常时,最好更精确。
正如我们将看到的,您根本不需要执行此合并。
git merge
主要是结合工作
让我们重新绘制您现在拥有的部分,只是一点点,以删除 master
部分。我们也可以将 origin/his-feature
视为一种 b运行ch。 name origin/his-feature
不是 b运行ch name,但它和一个一样好用,因为它保存链中最后一次提交的哈希 ID。
I--J <-- your-feature (HEAD)
/
...--G--H
\
K--L <-- origin/his-feature
当我们像这样有两个不同的 b运行 时,我们可能想要 合并工作 。这就是 git merge
命令的作用。让我们快速浏览一下它是如何的。
因为提交持有快照,而不是更改,我们必须将这些提交系列——这些以 J
和 L
结束的链—— 变成 变化。这意味着我们必须找到一些共同的起点。 both b运行ches 上有哪些提交?好吧,从图中可以很明显地看出这一点,前提是您意识到一次可以在多个 b运行ch 上提交。提交 H
和 G
,依此类推,在两个 b运行 上。
git merge
命令将找到这些提交中的最佳。幸运的是——就像在这种情况下一样——只有一个这样的最佳提交。此提交是 合并基础 。只要合并基础不是您要合并的两个提交之一,Git 就必须在此处进行真正的合并。在这种情况下 H
既不是 J
也不是 L
。所以现在 Git 比较 提交 H
中的快照与 J
中的快照,看看你改变了什么。它还将 H
中的快照与 L
中的快照进行比较,以查看它们发生了什么变化。 Git 然后组合这两组更改,将组合的更改应用到 H
中的快照,并使用结果制作新快照。
新快照——新提交——是一个合并提交。合并提交的唯一特殊之处在于它有两个 parent,而不是只有一个。 first parent 与任何新提交相同:您现在正在使用的提交。 second parent 是您在 git merge
命令中命名的提交。因此 git merge his-branch
将您的更改从 HEAD
/ commit J
(与 H
相比)与他的更改从 L
(与 H
).它将组合更改应用于 H
并进行新的合并提交 M
:
I--J
/ \
...--G--H M <-- your-feature (HEAD)
\ /
K--L <-- origin/his-feature
现在您已经合并了这两个 b运行ches。 (即使你的 name for commit L
不是 b运行ch name,提交链结束于 L
符合 b运行ch。另见 What exactly do we mean by "branch"?)
变基第 1 部分
git rebase
命令是我们见过的最复杂的命令。如果上面合并的快速扫描看起来很复杂,请注意,因为 rebase 更糟糕。有些人认为任何人都不应该使用 git rebase
。我不是这些人中的一员,但这是一个棘手的命令,您需要知道如何以及何时使用它,以及何时不使用它。到达那里的最好方法——知道什么时候使用它,什么时候不使用它——就是确切地知道它的作用。因为它太复杂了,我不能在这里详述所有的细节,但是让我们看一下你具体的 rebase运行:
git rebase -i HEAD~3
语法 HEAD~3
告诉 Git 从通过名称 HEAD
找到的提交中倒数三个 first-parents。鉴于我们已经绘制了这个:
I--J
/ \
...--G--H M <-- your-feature (HEAD)
\ /
K--L <-- origin/his-feature
并且我们知道提交 M
的 first parent 是 J
,我们得到的第一个 one-step-back是从 M
到 J
。下一步是从 J
到 I
。提交 J
只有一个 parent,所以它的第一个 parent 是它唯一的 parent,但这没关系:我们在提交 I
着陆并且已经返回两次.我们现在需要再倒退一次,从 I
到 H
。 git rebase -i
的参数现在可以解析为提交 H
.
的实际哈希 ID
git rebase
本身 所做的 是它复制了一些提交,就像通过 git cherry-pick
.8它复制的提交集部分取决于您给它的参数,在本例中,它指定提交 H
。这是第一个提交,它 不会 复制。它不会从 H
向后复制任何提交。9
本质上,git rebase
列出从 HEAD
开始并向后工作的提交,并且 在您列出 and/or 任何提交时停止 从那里。所以它最终列出提交 M
、J
-and-L
、I
-and-K
,然后命中 H
没有列出。因此,要复制的提交列表是 I
、J
、K
、L
和 M
.10
从这个列表中,git rebase
通常 完全删除 任何 合并提交 。由于 M
是合并提交,您的 git rebase
将删除它,留下提交 I-J
和 K-L
进行复制。它 可以 下降更多,但在这种情况下,它不会这样做。然后,您将在交互式 rebase 中得到一个命令列表,其中包含四个 11 pick
命令。这些代表 Git 到 运行 git cherry-pick
.
的指令
此时,git rebase
使用 Git 的 detached HEAD 模式(我们再次没有涉及)复制这四个提交中的每一个, 使用 git cherry-pick
或等价物。根据您的 Git 选择先复制您的提交还是先复制他的提交,以及您是否使用 --force
标志,您的 git rebase
可能会决定 re-use 一些提交,或者可能实际上复制它们。出于说明目的,我假设它结束 re-using 你的 I
和 J
,并复制他的 K-L
,但是它可以反其道而行之,或者复制所有这些。
同时,您告诉 Git 仅复制这两个提交中的 一个。为了便于说明,我将选择 L
进行复制。
8一些变基命令字面上使用git cherry-pick
。你所做的那种变基就是这样做的。其他人只是近似,但你仍然可以认为它们就像使用 cherry-pick 一样。我们没有在这里单独描述 cherry-pick,因为我不想让这个答案变得 真的 长。
9记住,提交通常是反向的。我们指定一些提交,使用 b运行ch 名称,或原始哈希 ID,或类似 HEAD~3
的名称表示向后计数,然后到达该提交后,Git 继续工作从那里向后。指定的提交是 Git 开始 的地方,然后根据需要在该提交之前进行其他操作。
10由于合并提交,这里的顺序很复杂。 git rebase -i
的实际顺序是 git rev-list
使用 --topo-order --reverse
产生的顺序,并且没有像我们希望的那样精确指定。幸运的是,使用 git rebase -i
,我们可以根据需要随意 re-shuffle 顺序。无论如何,这个细节可能对您的特定情况无关紧要。请注意合并提交时出现的陷阱,此处:顺序可能与您预期的不同。
11从你的HEAD~3
,我知道你在你的b[=908上有两个提交=]ch 未共享,在您使用 git pull
进行合并提交之前。我不知道他们有多少这样的承诺。你的 rebase 中的实际数字将比他们没有共享的数量多 2。
Rebase 第 2 部分:复制步骤
我们从这个开始:
I--J
/ \
...--G--H M <-- your-feature (HEAD)
\ /
K--L <-- origin/his-feature
并希望复制提交 I
。新的(据说是改进的)提交应该在 H
之后,而不是现在的位置。但是现有的 I
确实 在 H
之后。所以 rebase 很聪明,它对自己说:I can just re-use the existing I
. 它这样做了。现在它想复制 J
,以便新副本出现在 I
之后——但同样,它已经这样做了,所以只对现有的 re-use 进行变基 J
。此时,我们有:
I--J <-- HEAD
/ \
...--G--H M <-- your-feature
\ /
K--L <-- origin/his-feature
(这是“分离的 HEAD”模式在起作用,HEAD
直接指向提交)。现在 Git 想复制 L
,改进后的副本在 J
之后。现有的 L
在 K
之后,因此 Git 这次确实必须 复制 它。请记住,提交的 parent 在提交的元数据中,无法更改。提交 L
总是指向回提交 H
。所以 Git 将 L
复制到一个新的和改进的 L'
,像这样:
I--J--L' <-- HEAD
/ \
...--G--H M <-- your-feature
\ /
K--L <-- origin/his-feature
复制完成。
变基第 3 部分
git rebase
的最后一步 Git 提取了 b运行ch 名称——您开始时使用的名称,在本例中,your-feature
—关闭它现在指向的提交,使其指向最终复制的提交。所以这个 git rebase
的最后一步结果是:
I--J--L' <-- your-feature (HEAD)
/ \
...--G--H M ???
\ /
K--L <-- origin/his-feature
您的合并提交 M
发生了什么?答案是:它仍然存在,在您的 Git 存储库中。您只是 找不到 它。如果您在开始变基之前记下它的哈希 ID,则可以使用它来找到它。 Git 提供了找到它的方法,12 但现在您不必担心它们。由于您无法 查看 提交 M
,看起来您现在拥有:
I--J--L' <-- your-feature (HEAD)
/
...--G--H
\
K--L <-- origin/his-feature
你可以继续开发更多东西。
12这些包括 Git 的 reflogs,这是从错误的 rebase 或类似的恢复的常用方法.
当你们最终合并时会发生什么?
我们不知道,因为我们不知道如何你们最终会合并你们的工作。你会使用git merge
,还是会使用git rebase
?你的co-worker呢?
让我们假设,此时您又进行了两次提交:
I--J--L'-M--N <-- your-feature (HEAD)
/
...--G--H <-- master, origin/master
您将这些提交发送到 GitHub 服务器并在那里使用“拉取请求”机制。让我们进一步假设合并这个 PR 的人使用真正的合并。 (他们今天在 GitHub 上有三个选项;真正的合并是其中之一,这会强制 Git 执行合并,即使它可以执行 fast-forward。)结果将看起来像这样,在 GitHub Git:
I--J--L'-M--N [PR #123 was here, at commit N, but is done now]
/ \
...--G--H---------------O <-- master
\
K--L <-- his-feature
让我们假设他又做了两次提交,我们称之为 P
和 R
(跳过 Q
,因为它看起来太像 O
),并且将它们作为拉取请求发送到 GitHub Git:
I--J--L'-M--N
/ \
...--G--H---------------O <-- master
\
K--L--------P--R <-- PR#124
同样,负责合并的人可以选择使用哪个 GitHub 网络界面点击按钮。他们使用merge,还是rebase and merge,或者squash and merge?让我们假设他们使用定期合并。这次 Git 被迫进行真正的合并——fast-forwarding 是不可能的——真正的合并将使用提交 H
作为合并基础。它会将 H
中的快照与 O
中的快照进行比较,以查看一方做了什么,并将 H
中的快照与 R
中的快照进行比较,以查看一方做了什么另一边做了。它将合并这些更改并进行新的合并提交 S
:
I--J--L'-M--N
/ \
...--G--H---------------O--S <-- master
\ /
K--L--------P--R
没有提交 丢失,但在可能有点丑陋的地方,提交 L
和 L'
都是 在历史上。那段历史反映了现实:你真的做了 复制了他的提交 L
到你的提交 L'
。提交 L'
上有你的名字:你是 提交者 而你的 co-worker 是 作者.13 所以有些人认为这是最好的方法。
还有很多其他方法可以处理这个问题。 None object 绝对是“最好的”。 Git 提供 工具。它没有规定特定的work-flows。它并没有规定最终的提交集应该是什么:这取决于你。 GitHub 提供特定的 work-flows(通过他们的点击按钮),但仍然有很多操作按钮的人,并且直接使用 Git,你可以——前提是你对 GitHub——做任何Git能做的事情。
13当您 运行 git cherry-pick
时,您可以控制其中的某些行为,但那是 权利信息,所以你应该这样离开。当使用 git rebase
时,很难对个人 cherry-pick 大惊小怪。
我对 git 有点陌生,正在尝试了解 branches/rebase 的工作原理,如果有可能我搞砸了。
我和我的同事都在不同的分支机构,我需要他为我的工作所做的更改。所以我取消了他的更改。然后我做了
git rebase -i HEAD~3
并且刚刚删除了我不需要的他的提交。然后我向我们的回购推送并创建了一个拉取请求,一切看起来都不错。看起来就像我需要他的更改以及我自己的更改的一次提交。
我还没有合并。是否有可能一旦他合并,然后我合并,不知何故,因为我删除了他在我正在合并的分支上的提交,它删除了他的代码?
许多事情 可能 Git。要了解 实际上 与 Git 发生了什么,您必须了解 b运行ch 是什么以及为您做什么。这实际上很少,要理解 that,您必须了解 Git commit 是什么以及为您做什么。
在这种特殊情况下,我认为您和您的 co-worker 是安全的,但这实际上取决于你们如何使用各种 Git 存储库。请注意,您和他有 单独的 Git 存储库 。您可能还都使用存储在其他地方的 third 存储库,例如 GitHub。您没有共享 b运行ches,也没有共享各种存储库本身的任何其他内容,这主要是一件好事。这意味着您实际共享的唯一内容是 提交 .
这里提一下,b运行ch name主要是一种存储一个(单个)ID的方式commit。 b运行ch 名称也有很多辅助用途,但是存储提交哈希 ID 是任何 Git 的各种名称的本质——b运行ch 名称,标签名称和其他名称。这里有一个主题:一切都是关于提交。 Git 与 b运行ches 无关,尽管 b运行ch 名称可以帮助您 find 提交; Git 与文件无关,但提交 包含 文件。最后,一切都与提交有关。
什么是提交?
提交是一种存储一堆文件的快照的方式——我们称之为数据,或者通常称为树——以及关于提交的其他信息,我们称之为元数据。因此,提交是存储这两项的东西:源快照或树,以及有关提交的信息,例如提交人、时间和原因(他们的日志消息)。但是我们需要更仔细地看一下,看看每个提交里面有什么,以及我们如何找到一个提交。
每个提交都有一个唯一的编号。这个数字不是一个很好的简单顺序计数:我们没有提交 #1,然后是 #2、#3 等等。相反,该数字是一个丑陋的大 哈希 ID,以十六进制表示。它看起来 运行dom,但它不是。
任何一次提交的哈希 ID 实际上是该提交全部内容的加密校验和:数据和元数据。因为每个 Git 都以相同的方式计算这个 ID,所以两个 Git 可以通过比较哈希 ID 来判断一个是否有另一个的提交。 Git gua运行tees1 这个哈希 ID 是唯一的,除其他外,在元数据中为每个提交添加一个 date-and-time-stamp,因此,即使您保存相同的文件,使用相同的用户名和电子邮件地址等,除非您在相同的 time[=601= 保存文件,否则您将获得一个新的和不同的提交] 两次——如果你这样做了,你怎么知道你做了两次?
Git 通过哈希 ID 查找提交和其他内部 Git object 也使用此哈希方案的提交。实际上,Git 的大部分是一个简单的 key-value database,其键是散列 ID,内容是 object。2 这提供了一个data-integrity 校验:给定一个密钥,Git 查找数据,然后确保存储的数据仍然是密钥的校验和。
这样做的一个重要结果是每次提交后,都是 100% read-only。如果你把数据库的任何 object out 搞砸了,即使是一点点的改变,然后写回来,你得到的是一个新的和不同的 object 使用新密钥。3 因此,当您使用提交执行此操作时,您只需添加一个新提交。旧提交保留在数据库中。4
提交中的文件同样是 read-only,并且会永久存储(但请参阅脚注 4)。它们是压缩的 Git-only 格式,为了解决在每个快照中存储每个文件会使数据库增长得非常快的问题,它们都是自动 de-duplicated(见脚注 3)。因此,如果一千次提交重复使用相同版本的 README.md
,那么数据库中实际上只有一个副本。
关于提交的最后一件事——你只需要记住这些部分,真的——是每个提交在其元数据中存储一些前一个的哈希ID提交或提交。大多数提交只存储一个先前的提交哈希 ID。这是提交的 parent.
总结:
- 每个提交都由其哈希 ID 编号。
- 每个提交都存储数据——所有文件的快照,但压缩文件和de-duplicated——和元数据,包括一些先前提交的哈希 ID。
- 所有这些都是read-only。你不能直接看到这些东西,甚至不能直接使用提交中的文件,因为使用它们都是这种内部格式。
1pigeonhole principle tells us that this must eventually fail. The hash IDs are as big and ugly as they are to make sure that it won't accidentally fail in any sensible amount of time. It's possible to make this fail intentionally,所以Git正在从SHA-1移动到SHA-256,但同时这在实践中不是问题.
2提交是四种内部类型之一 object。为了完整起见,其他三个是 blob、tag(或带注释的标签)和 tree.
3如果数据与数据库中已有的数据匹配,Git 只是说 aha, a duplicate 而不是根本不用存储它。例如,这就是 Git de-duplicates 存储文件的方式。提交通过 time-stamps 和其他与之前提交不匹配的元数据获得唯一性。
4这意味着数据库只会增长。 是一种丢弃未使用的东西的方法,但我们不会在这里详细介绍。
B运行通道名称
“提交存储它们的 parent 哈希 ID”的一个主要结果是提交形成了一个 向后看的链 。也就是说,假设我们有一系列提交,并且我们使用单个大写字母代表哈希 ID。我们可以这样画:
... <-F <-G <-H
其中 H
是某个提交链中 last 提交的哈希 ID。提交 H
存储快照和元数据,在元数据中,H
存储早期提交 G
的哈希 ID。我们说H
指向G
,或者说G
是H
的parent。 G
也有快照和元数据,在 G
中我们将找到早期提交 F
的哈希 ID。 F
依次指向另一个更早的提交,依此类推。这一切一直持续到我们到达“第一个”提交,它没有 parent。所以我们只需要 H
的实际哈希 ID。从那里,我们可以找到 每个较早的提交 .
但是有一个问题:我们在哪里可以找到提交 H
的哈希 ID?我已经放弃了:它在 b运行ch name 中。这就是 b运行ch 名称的含义和作用:它包含某些提交链中 last 提交的哈希 ID。所以我们可以这样绘制提交:
..--G--H <-- branch
关于 Git b运行ch 名称的有趣之处在于它们 移动 。让我们创建一个新的 b运行ch 名称,例如 feature
,并让它也指向 H
:
..--G--H <-- branch, feature
现在我们需要知道使用哪个b运行ch名称,所以让我们将特殊名称HEAD
附加到一个b运行通道名称:
..--G--H <-- branch (HEAD), feature
这告诉我们,我们将使用 通过name branch
提交H
。如果我们 运行:
git checkout feature
我们将继续使用提交 H
,但是通过 name feature
:
..--G--H <-- branch, feature (HEAD)
假设我们现在进行 new 提交。不谈太多细节,我们只说 Git 为新提交分配了一个新的哈希 ID,我们将其称为 I
。 Git 会将 I
的 parent 设置为 H
,因此我们可以这样绘制:
..--G--H
\
I
现在我们看到了 Git 的特殊技巧:当 Git 进行了这个新提交时,Git 将其哈希 ID 写入 HEAD
附上:
..--G--H <-- branch
\
I <-- feature (HEAD)
这就是 b运行ch 增长的方式——好吧,一种方式:每次我们进行新的提交时,Git 都会更新 name 以指向到我们刚刚做出的新承诺。新提交指向刚才名称指向的位置。
T运行将提交提交到另一个存储库
我们在上面看到了 git commit
如何进行新提交的简要视图。 git merge
命令也可以像其他各种命令一样进行新的提交。完成这些新提交后,您可能希望 与其他 Git 用户共享 它们,例如您的 co-worker。为此,您必须让您的 Git 调用其他 Git。您将使用 URL 执行此操作:一些服务将回答 URL 并调用另一个 Git.
为了避免每次都输入长的 URL,Git 将把 URL 存储在像 origin
这样的短名称下。我们称这个简称为 remote。您可以使用 git fetch
调用远程,这意味着 从他们那里获取我的提交 ,或 git push
,这意味着 让我发送提交给他们。这些操作非常相似,当然方向相反,但最后它们也完全不同。两者都是通过让您的 Git 调用他们的 Git 开始的,之后他们的 Git 可能会列出 他们的 b运行ch 名称,以及与这些名称一起使用的哈希 ID。5 现在您的 Git 知道他们的 Git 有什么——b运行ch 名称和哈希 ID —实际 t运行sfer 开始:
对于
git fetch
,此列表构成一个产品:您可以提交这些内容。您的 Git 开始如果您还没有那些特定的提交并且您想要 b运行ch,请通过他们的哈希 ID 向他们的 Git 询问特定的提交。如果您的 Git 要求一些提交,他们的 Git 现在必须提供该提交的 parent 或 parents,并且再次,你的 Git 要么说 请发送那个提交 要么 不,谢谢,我已经有了那个 .使用
git push
,您的 Git 提供 last 提交,通过哈希 ID,来自您的 b运行ches(假设您使用git push origin <em>someb运行ch</em>
,即)。他们的Git要么说请发送,要么说不,谢谢,就像上面一样。如果需要,这反过来会导致您的 Git 提供其 parent,依此类推。现在发送方知道要发送什么以及接收方已经拥有什么,发送方打包要发送的提交以及接收方没有的任何文件。通过了解接收方 确实 的提交——发送方也有相同的提交,它们具有相同的文件——发送方可以发送 slimmed-down 包。6 接收方修复了该包裹以根据需要填充任何缺失的部分(详细信息远远超出了本答案的范围)。
最后,无论谁是接收者都必须设置一些名字或名字来记住last 在每个 b运行ch 中提交。在这里,fetch和push又分道扬镳了。
使用 git fetch
,当您的 Git 有他们的 b运行ch 提示,并且知道他们的 b运行ch names,你的 Git 所做的是 重命名 他们的兄弟 运行 。他们的兄弟运行是他们的,而不是你的,如果他们更新了他们的 develop
,那不会例如,这并不意味着您希望自己的 develop
b运行ch 名称从 您的 新提交中删除。因此,如果他们有 develop
,您的 Git 会创建或更新您的 origin/develop
,而不是您的 develop
。这假设您正在执行 git fetch origin
。如果您为遥控器使用其他名称,例如 bob
,您将得到 bob/develop
,例如。
与 git push
另一方面,您的 Git 要求他们的 Git 设置他们的一个 b运行ch 名称。这需要特别小心;我们将在下面看到更多相关信息。
5这一步已经成为一些long-lived存储库的痛点,这些存储库有很多b运行ches和标签,更新的Git 协议允许 server-side 过滤名称,以避免在此处列出完整的名称。否则,您可能会花 10 分钟来获取 name-and-hash-ID 信息,最后却决定,您的 Git 只需要一次提交,而 t运行 会在几秒钟内完成。但是原理还是一样的
6从技术上讲,发件人通常会在这里构建 Git 所谓的 thin pack。有些协议根本不支持包:发送方必须发送单独的 objects,这要慢得多。不过,现代 t运行sfer 协议都使用精简包。
既然我们知道了这么多,让我们观察一个git fetch
的动作
假设您的存储库中有这个:
I--J <-- your-feature (HEAD)
/
...--G--H <-- master, origin/master
与此同时,您的 co-worker 在他的存储库中有这个:
...--G--H <-- master, origin/master
\
K--L <-- his-feature (HEAD)
请记住,每个哈希 ID 都是唯一的,因此只有 shared...--G--H
提交在 both 存储库中。
他现在 运行s git push origin his-feature
,将他的提交 K-L
发送到 GitHub 存储库,并创建一个新的 b运行ch 名称那里,his-feature
,所以 GitHub 上的第三个存储库有这个:
...--G--H <-- master
\
K--L <-- his-feature
请注意,我们不知道或不关心 GitHub 存储库在哪个 b运行ch 上(它实际上可能在其 master
上,但它是 bare 存储库,这并不意味着什么)。 GitHub 存储库没有 origin/*
名称,因为它没有从其他存储库复制名称:GitHub 上没有人以管理员身份访问该存储库,运行s git fetch
.
如果您现在 运行 git fetch origin
,您的 Git 联系 GitHub Git,它列出了它的 b运行ch 名称及其相应的提交:master
,即提交H
,和his-feature
,即提交L
。你的 Git 已经有 H
,所以它不需要任何东西,但是你的 Git 要求 L
然后 K
(然后又一次有H
已经)。因此,您将 K-L
添加到您的存储库,然后 您的 Git 创建您自己的 origin/his-branch
。您现在拥有:
I--J <-- your-feature (HEAD)
/
...--G--H <-- master, origin/master
\
K--L <-- origin/his-feature
请注意,您正在处理或使用的任何内容都没有改变。您拥有与之前相同的所有提交,加上 两个新提交。您的 b运行ch 名称中的 None 已更改,但您已获得此 origin/his-feature
名称。7
7如果你用的是很旧的Git 的版本早于 1.8.4,并且您使用 git pull
而不是 git fetch
,您不会获得 origin/*
名称。在这种情况下,我建议避免使用 git pull
或升级 Git 版本。这并不重要,但在 Git 版本中落后那么多充其量是令人讨厌的:从那以后你错过了很多改进。
git pull
表示运行git fetch
,然后运行第二个Git命令
您之前提到过您:
pulled his changes
我假设你的意思是你 运行 git pull
使用 b运行ch 名称 his-feature
。我个人不喜欢 git pull
命令,因为它非常棘手:它首先 运行s git fetch
,这更直接,但然后立即 运行s 第二 Git命令。你必须选择 Git 命令到 运行 之前 你有机会看到 git fetch
实际上 做了什么.但是在 git fetch
之后,你经常想使用 git merge
,这是默认的第二个命令。因此,让两个命令 运行 一个紧接着另一个是很方便的——好吧,有时,或者很多时候。这就是 git pull
为您所做的。所以现在是时候看看 git merge
.
但是请注意,您并没有真正提取他的更改:相反,您获取了他的提交——提交保存快照,而不是变化——然后合并。这是处理更改的 merge 步骤。说 pull his changes 有点草率。在平时的谈话中还好,但是当你想到你在做什么,并把它画出来以确保一切正常时,最好更精确。
正如我们将看到的,您根本不需要执行此合并。
git merge
主要是结合工作
让我们重新绘制您现在拥有的部分,只是一点点,以删除 master
部分。我们也可以将 origin/his-feature
视为一种 b运行ch。 name origin/his-feature
不是 b运行ch name,但它和一个一样好用,因为它保存链中最后一次提交的哈希 ID。
I--J <-- your-feature (HEAD)
/
...--G--H
\
K--L <-- origin/his-feature
当我们像这样有两个不同的 b运行 时,我们可能想要 合并工作 。这就是 git merge
命令的作用。让我们快速浏览一下它是如何的。
因为提交持有快照,而不是更改,我们必须将这些提交系列——这些以 J
和 L
结束的链—— 变成 变化。这意味着我们必须找到一些共同的起点。 both b运行ches 上有哪些提交?好吧,从图中可以很明显地看出这一点,前提是您意识到一次可以在多个 b运行ch 上提交。提交 H
和 G
,依此类推,在两个 b运行 上。
git merge
命令将找到这些提交中的最佳。幸运的是——就像在这种情况下一样——只有一个这样的最佳提交。此提交是 合并基础 。只要合并基础不是您要合并的两个提交之一,Git 就必须在此处进行真正的合并。在这种情况下 H
既不是 J
也不是 L
。所以现在 Git 比较 提交 H
中的快照与 J
中的快照,看看你改变了什么。它还将 H
中的快照与 L
中的快照进行比较,以查看它们发生了什么变化。 Git 然后组合这两组更改,将组合的更改应用到 H
中的快照,并使用结果制作新快照。
新快照——新提交——是一个合并提交。合并提交的唯一特殊之处在于它有两个 parent,而不是只有一个。 first parent 与任何新提交相同:您现在正在使用的提交。 second parent 是您在 git merge
命令中命名的提交。因此 git merge his-branch
将您的更改从 HEAD
/ commit J
(与 H
相比)与他的更改从 L
(与 H
).它将组合更改应用于 H
并进行新的合并提交 M
:
I--J
/ \
...--G--H M <-- your-feature (HEAD)
\ /
K--L <-- origin/his-feature
现在您已经合并了这两个 b运行ches。 (即使你的 name for commit L
不是 b运行ch name,提交链结束于 L
符合 b运行ch。另见 What exactly do we mean by "branch"?)
变基第 1 部分
git rebase
命令是我们见过的最复杂的命令。如果上面合并的快速扫描看起来很复杂,请注意,因为 rebase 更糟糕。有些人认为任何人都不应该使用 git rebase
。我不是这些人中的一员,但这是一个棘手的命令,您需要知道如何以及何时使用它,以及何时不使用它。到达那里的最好方法——知道什么时候使用它,什么时候不使用它——就是确切地知道它的作用。因为它太复杂了,我不能在这里详述所有的细节,但是让我们看一下你具体的 rebase运行:
git rebase -i HEAD~3
语法 HEAD~3
告诉 Git 从通过名称 HEAD
找到的提交中倒数三个 first-parents。鉴于我们已经绘制了这个:
I--J
/ \
...--G--H M <-- your-feature (HEAD)
\ /
K--L <-- origin/his-feature
并且我们知道提交 M
的 first parent 是 J
,我们得到的第一个 one-step-back是从 M
到 J
。下一步是从 J
到 I
。提交 J
只有一个 parent,所以它的第一个 parent 是它唯一的 parent,但这没关系:我们在提交 I
着陆并且已经返回两次.我们现在需要再倒退一次,从 I
到 H
。 git rebase -i
的参数现在可以解析为提交 H
.
git rebase
本身 所做的 是它复制了一些提交,就像通过 git cherry-pick
.8它复制的提交集部分取决于您给它的参数,在本例中,它指定提交 H
。这是第一个提交,它 不会 复制。它不会从 H
向后复制任何提交。9
本质上,git rebase
列出从 HEAD
开始并向后工作的提交,并且 在您列出 and/or 任何提交时停止 从那里。所以它最终列出提交 M
、J
-and-L
、I
-and-K
,然后命中 H
没有列出。因此,要复制的提交列表是 I
、J
、K
、L
和 M
.10
从这个列表中,git rebase
通常 完全删除 任何 合并提交 。由于 M
是合并提交,您的 git rebase
将删除它,留下提交 I-J
和 K-L
进行复制。它 可以 下降更多,但在这种情况下,它不会这样做。然后,您将在交互式 rebase 中得到一个命令列表,其中包含四个 11 pick
命令。这些代表 Git 到 运行 git cherry-pick
.
此时,git rebase
使用 Git 的 detached HEAD 模式(我们再次没有涉及)复制这四个提交中的每一个, 使用 git cherry-pick
或等价物。根据您的 Git 选择先复制您的提交还是先复制他的提交,以及您是否使用 --force
标志,您的 git rebase
可能会决定 re-use 一些提交,或者可能实际上复制它们。出于说明目的,我假设它结束 re-using 你的 I
和 J
,并复制他的 K-L
,但是它可以反其道而行之,或者复制所有这些。
同时,您告诉 Git 仅复制这两个提交中的 一个。为了便于说明,我将选择 L
进行复制。
8一些变基命令字面上使用git cherry-pick
。你所做的那种变基就是这样做的。其他人只是近似,但你仍然可以认为它们就像使用 cherry-pick 一样。我们没有在这里单独描述 cherry-pick,因为我不想让这个答案变得 真的 长。
9记住,提交通常是反向的。我们指定一些提交,使用 b运行ch 名称,或原始哈希 ID,或类似 HEAD~3
的名称表示向后计数,然后到达该提交后,Git 继续工作从那里向后。指定的提交是 Git 开始 的地方,然后根据需要在该提交之前进行其他操作。
10由于合并提交,这里的顺序很复杂。 git rebase -i
的实际顺序是 git rev-list
使用 --topo-order --reverse
产生的顺序,并且没有像我们希望的那样精确指定。幸运的是,使用 git rebase -i
,我们可以根据需要随意 re-shuffle 顺序。无论如何,这个细节可能对您的特定情况无关紧要。请注意合并提交时出现的陷阱,此处:顺序可能与您预期的不同。
11从你的HEAD~3
,我知道你在你的b[=908上有两个提交=]ch 未共享,在您使用 git pull
进行合并提交之前。我不知道他们有多少这样的承诺。你的 rebase 中的实际数字将比他们没有共享的数量多 2。
Rebase 第 2 部分:复制步骤
我们从这个开始:
I--J
/ \
...--G--H M <-- your-feature (HEAD)
\ /
K--L <-- origin/his-feature
并希望复制提交 I
。新的(据说是改进的)提交应该在 H
之后,而不是现在的位置。但是现有的 I
确实 在 H
之后。所以 rebase 很聪明,它对自己说:I can just re-use the existing I
. 它这样做了。现在它想复制 J
,以便新副本出现在 I
之后——但同样,它已经这样做了,所以只对现有的 re-use 进行变基 J
。此时,我们有:
I--J <-- HEAD
/ \
...--G--H M <-- your-feature
\ /
K--L <-- origin/his-feature
(这是“分离的 HEAD”模式在起作用,HEAD
直接指向提交)。现在 Git 想复制 L
,改进后的副本在 J
之后。现有的 L
在 K
之后,因此 Git 这次确实必须 复制 它。请记住,提交的 parent 在提交的元数据中,无法更改。提交 L
总是指向回提交 H
。所以 Git 将 L
复制到一个新的和改进的 L'
,像这样:
I--J--L' <-- HEAD
/ \
...--G--H M <-- your-feature
\ /
K--L <-- origin/his-feature
复制完成。
变基第 3 部分
git rebase
的最后一步 Git 提取了 b运行ch 名称——您开始时使用的名称,在本例中,your-feature
—关闭它现在指向的提交,使其指向最终复制的提交。所以这个 git rebase
的最后一步结果是:
I--J--L' <-- your-feature (HEAD)
/ \
...--G--H M ???
\ /
K--L <-- origin/his-feature
您的合并提交 M
发生了什么?答案是:它仍然存在,在您的 Git 存储库中。您只是 找不到 它。如果您在开始变基之前记下它的哈希 ID,则可以使用它来找到它。 Git 提供了找到它的方法,12 但现在您不必担心它们。由于您无法 查看 提交 M
,看起来您现在拥有:
I--J--L' <-- your-feature (HEAD)
/
...--G--H
\
K--L <-- origin/his-feature
你可以继续开发更多东西。
12这些包括 Git 的 reflogs,这是从错误的 rebase 或类似的恢复的常用方法.
当你们最终合并时会发生什么?
我们不知道,因为我们不知道如何你们最终会合并你们的工作。你会使用git merge
,还是会使用git rebase
?你的co-worker呢?
让我们假设,此时您又进行了两次提交:
I--J--L'-M--N <-- your-feature (HEAD)
/
...--G--H <-- master, origin/master
您将这些提交发送到 GitHub 服务器并在那里使用“拉取请求”机制。让我们进一步假设合并这个 PR 的人使用真正的合并。 (他们今天在 GitHub 上有三个选项;真正的合并是其中之一,这会强制 Git 执行合并,即使它可以执行 fast-forward。)结果将看起来像这样,在 GitHub Git:
I--J--L'-M--N [PR #123 was here, at commit N, but is done now]
/ \
...--G--H---------------O <-- master
\
K--L <-- his-feature
让我们假设他又做了两次提交,我们称之为 P
和 R
(跳过 Q
,因为它看起来太像 O
),并且将它们作为拉取请求发送到 GitHub Git:
I--J--L'-M--N
/ \
...--G--H---------------O <-- master
\
K--L--------P--R <-- PR#124
同样,负责合并的人可以选择使用哪个 GitHub 网络界面点击按钮。他们使用merge,还是rebase and merge,或者squash and merge?让我们假设他们使用定期合并。这次 Git 被迫进行真正的合并——fast-forwarding 是不可能的——真正的合并将使用提交 H
作为合并基础。它会将 H
中的快照与 O
中的快照进行比较,以查看一方做了什么,并将 H
中的快照与 R
中的快照进行比较,以查看一方做了什么另一边做了。它将合并这些更改并进行新的合并提交 S
:
I--J--L'-M--N
/ \
...--G--H---------------O--S <-- master
\ /
K--L--------P--R
没有提交 丢失,但在可能有点丑陋的地方,提交 L
和 L'
都是 在历史上。那段历史反映了现实:你真的做了 复制了他的提交 L
到你的提交 L'
。提交 L'
上有你的名字:你是 提交者 而你的 co-worker 是 作者.13 所以有些人认为这是最好的方法。
还有很多其他方法可以处理这个问题。 None object 绝对是“最好的”。 Git 提供 工具。它没有规定特定的work-flows。它并没有规定最终的提交集应该是什么:这取决于你。 GitHub 提供特定的 work-flows(通过他们的点击按钮),但仍然有很多操作按钮的人,并且直接使用 Git,你可以——前提是你对 GitHub——做任何Git能做的事情。
13当您 运行 git cherry-pick
时,您可以控制其中的某些行为,但那是 权利信息,所以你应该这样离开。当使用 git rebase
时,很难对个人 cherry-pick 大惊小怪。