在某些情况下,我对功能分支进行了 git 拉取,最终我有多个烦人的"合并提交",我可以理解它们为什么会发生,但我想合并它们以看起来像一个正常的提交。
我尝试使用git rebase -i --rebase-merges HEAD~4
但无法弄清楚如何压制合并提交。
我做了进一步的研究,经过大量的挖掘,我能够执行以下操作,使用变基将不需要的合并提交合并到正常的提交中,然后在需要时压缩它们:
git checkout feature
git pull # will create merge commits
git checkout featur_backup # to create a backup
git switch --orphan emty_commit
git commit -m "First empty commit for the feature branch" --allow-empty
git switch feature
git rebase empty_commit
git rebase -i --root # this allows you to squash commits
git branch -D empty_commit
有没有更好的方法来合并合并提交?
笔记:
- 功能分支是孤立分支
- 文件从主分支单独检入功能分支
- 此功能分支用于编译要在目标计算机上应用的更改 功能
- 分支中来自不同的机器进行更改,这就是为什么我们最终会在 git 拉取后看到合并提交。
Ôrel的答案有一个食谱。 你可能不想直接origin/master
——你想要什么取决于你,你可能希望考虑这个问题并尝试一下,一旦你通读并解决了这个解释的部分——但变基确实有效。
我仍然不明白你提到的 git 变基将如何工作?我的问题是,当我对功能分支进行 git 拉取时,它最终会进行合并提交,那么
git rebase origin/master
将如何工作?
这里的关键是同时理解许多事情。 (这通常是 Git 的情况。 您需要知道:
- 个人如何提交工作;
- 分支名称的工作原理;
git fetch
如何运作;git merge
如何运作;git pull
表示运行git fetch
,然后运行git merge
或其他第二个命令(您一直在使用git merge
);git rebase
如何运作。
这是很多东西! 我们不能指望涵盖所有内容,即使是在我著名的1长答案之一中也是如此,因此我们将通过几个关键项目进行比赛。
1或您选择的其他副词。
提交、分支和分支名称
提交:
-
已编号:它具有唯一的哈希 ID。 这个数字意味着提交,不仅在你的仓库中,而且在每个仓库中,甚至是所有没有你提交的 Git 仓库。 (这是为 Git 提供动力的主要深层魔法。
-
是只读的:任何提交的任何部分都不能更改。 这是幻数系统所要求的。
-
包含两件事:所有文件的完整快照(间接存储,以特殊的 Git 化形式存储,其中它们被压缩和删除重复,因此当新提交重用以前提交的几乎所有文件时,这是一件好事:这意味着它们不占用空间),以及元数据:有关提交的信息。
元数据包含您的姓名和电子邮件地址以及您提交日期和时间等内容。 出于 Git 自己的目的,Git 在任何一个提交的元数据中存储以前提交的哈希 ID 列表。 大多数提交(我们倾向于称之为"普通"提交)在这里只有一个哈希 ID。 这会将普通提交形成一个简单的链,只是连接提交的箭头是向后而不是向前的。 人类喜欢将箭头视为前进,但这在 Git 中不起作用,因为提交是严格只读的。
这意味着给定一行中的一串提交,每个提交都有自己的哈希 ID,我们可以将提交字符串、较新的提交绘制到右侧,如下所示:
... <-F <-G <-H
这里H
代表链中最后一个提交的哈希 ID。 提交H
包含每个文件的完整快照,以及一些元数据。 在H
的元数据中,Git 存储了一些早期提交G
的哈希 ID,我们(和 Git)称之为提交H
的父级。 因此,提交H
指向较早的提交G
。
当然,G
也是一个提交,所以它有一个快照和元数据,并且该元数据保存了一些更早的父F
的哈希 ID。 但F
也是一个承诺,所以F
也指向后退。 这种情况永远持续下去,或者更确切地说,直到我们到达有史以来的第一次提交,这是第一次,没有父母(一种奇怪的"处女出生";Git 称其为根提交)。
提交都存储在一个大数据库中("所有 Git 对象",包括提交对象 - 其他类型的对象大多支持提交的东西,即树和 blob 对象,以及带注释的标记对象)。 此数据库是一个简单的键值存储,其中键是哈希 ID。 所以 Git 需要一个哈希 ID 来查找提交。
所有这些都有两个重要的含义:
提交不存储差异。 我们得到差异——我们将提交视为一种变化——通过让 Git比较相邻的提交。 我们选择一些父/子对,例如
G
和H
,并让 Git 比较两个快照。 Git 使用的重复数据删除技巧使 Git 可以轻松地立即丢弃所有完全相同的文件,因此 Git 只需要弄清楚两个实际上不同的文件中发生了哪些更改。 这通常不会花费太长时间,因此即使 Git 只存储了快照,git show
或git log -p
也可以显示"补丁"。Git 可以使用存储在每个提交中的父项自行查找除最后一个提交之外的每个提交。 我们必须告诉 Git 提交
H
的哈希 ID。 从那里开始,Git 自行向后工作。 但是我们必须在这里提供一个原始的哈希ID,这对人类来说是可怕的:哈希ID看起来是随机的,没有办法弄清楚一个。 你必须记住它们,或者把它们写下来,或者其他什么。
为了处理最后一个问题,Git 提供了一个单独的键值数据库,它按名称键控:分支名称、标签名称和许多其他类型的名称。在此数据库中,与任何给定名称关联的值是一个哈希 ID。 你只得到一个哈希 ID——不是两个或三个或多个,只有一个——但这就是我们所需要的,因为我们只需要存储最新提交的哈希 ID,例如,提交H
。
由于我们说提交H
"指向"其父G
,我们同样说分支名称指向分支中的最后一个提交。 Git 对此的术语是H
是提示提交,我们可以像这样将其添加到我们的绘图中:
...--G--H <-- main # or master or whatever
当我们"在"某个分支上时,我们会为该分支名称附加一个特殊名称 -HEAD
。 (Git 字面上只是将分支的名称存储在名为HEAD
.git
的文件中,至少对于主工作树而言,尽管您不应该依赖它,以防万一 Git 的未来版本想出更好/更漂亮的方法来做到这一点。 这意味着,如果我们有多个分支名称,都指向提交H
——这在 Git 中是完全正常的事情——那么当我们添加提交时,只有HEAD
分支名称会更新。 我们可以从例如:
...--G--H <-- feature (HEAD), main, zorg
然后进行一个新的提交——它得到一些新的、唯一的哈希 ID,但我们只称它为"提交I
"——并且git commit
命令使 Git 将新的哈希 ID 存储在当前分支名称中,以便我们得到:
...--G--H <-- main, zorg
I <-- feature (HEAD)
特殊名称HEAD
仍附加到当前分支名称。 名称feature
现在选择I
作为其提示提交;I
指向后H
,因为当我们I
时H
是feature
的尖端;H
和G
等等都保持不变(它们必须是,因为它们都是只读的)。 下一次提交再次更新当前分支名称:
...--G--H <-- main, zorg
I--J <-- feature (HEAD)
新提交J
点回到I
,指向H
,依此类推。I
一旦我们成功了就无法更改:任何提交的任何部分都无法更改。 请注意,虽然有些人会将提交I-J
称为"feature
上的内容",但实际上,所有提交都在feature
上:只是提交到并包括H
也在其他分支上,而I-J
目前仅在feature
上。
git fetch
, or, 提交是通用的,但分支名称不是
当我们克隆一个仓库时,我们复制它的所有提交2而不是它的任何分支名称。 我们没有复制它的分支名称,而是采用他们(其他仓库的)分支名称并将其更改为远程跟踪名称:例如,它们的main
成为我们的origin/main
,他们的feature
成为我们的origin/feature
。 然后我们的 Git 将在我们的新仓库中创建一个分支名称,其中包含它们的所有提交和这些修改后的名称。 新的分支名称将匹配其分支名称之一,并将选择与其名称相同的提示提交,因此我们的图片可能如下所示:
...--G--H <-- main (HEAD), origin/main
I--J <-- origin/feature
取决于当我们运行 Git 存储库时他们在 Git 存储库中的内容git clone
。
现在,我们也可以创建自己的feature
名称,并切换到它:
...--G--H <-- main, origin/main
I--J <-- feature (HEAD), origin/feature
如果我们进行新的提交,它们会增加我们的feature
。 不过,我们对他们feature
的记忆仍然指向承诺J
:
...--G--H <-- main, origin/main
I--J <-- origin/feature
K--L <-- feature (HEAD)
这些看起来完全像分支,因为它们完全像分支,这取决于我们所说的分支是什么意思(另请参阅我们所说的">分支"到底是什么意思?)。 在某种程度上,"分支"的意思是"通过从某个名称开始并向后工作找到的一组提交",远程跟踪名称作为分支工作得很好。 (但它也不是一个分支,因为你不能git switch
它。 那么它是一个分支吗? 这取决于你所说的分支是什么意思。 分支这个词的这个问题就是为什么你在说">分支"时必须小心——你可能知道你的意思,但其他人会吗? 你甚至确定你明白你的意思吗? 人类的认知是松散的,草率的措辞可能会导致错误的结论。
在任何情况下,您的远程跟踪名称都会反映其分支名称,截至上次您的 Git 软件调用其 Git 存储库并询问他们这些分支名称时。 但是,如果是几个月,几天,几小时,甚至几秒钟,也许他们的存储库已经改变。 要重新同步,请运行git fetch
:
- 您的 Git 软件会调用他们的 Git 软件。
- 他们列出他们的分支名称并提交哈希 ID。
- 您的 Git(在您的存储库上运行的软件)会检查您是否有这些提交(即,您是否有这些哈希 ID)。 对于你缺少的人,你的 Git 会带他们过来,以及所有需要的父母和祖父母等等。 现在,您拥有了所有自己的提交,以及他们没有的任何新提交。
- 最后,您的 Git 会使用新的记忆更新您的远程跟踪名称。 (请注意,这一步可以禁止,古代版本的 Git 做得不如现代 Git。
因此,在git fetch
之后,如果他们提供了新的提交,您可能会:
...--G--H <-- main, origin/main
I--J----N--O <-- origin/feature
K--L <-- feature (HEAD)
正如您的origin/feature
所记得的那样,您的feature
现在在他们的feature
后面,因为提交N
并且O
不在您的feature
上。 您的feature
也领先于他们的feature
两个提交:您的K-L
。 但"落后"可能是一个问题。
2好吧,全部或大部分:我们不会在这里进行细微的区分。 请注意,这有点过于概括。
<小时 />git merge
的工作原理
让我们退后一点,只考虑一下您自己的个人存储库。 假设您有两个分支名称,br1
和br2
,排列方式如下:
I--J <-- br1
/
...--G--H
K--L <-- br2
您想使用git merge
将这两个分支绑定在一起。 您可以选择其中之一并切换到它,例如,git switch br1
或git checkout br1
:
I--J <-- br1 (HEAD)
/
...--G--H
K--L <-- br2
然后你运行git merge br2
. 这样做的最终结果是:
I--J
/
...--G--H M <-- br1 (HEAD)
/
K--L <-- br2
之后,您可以自由删除名称br2
,因为它的提示提交L
现在可以从提交M
中找到。 您不必删除该名称,但您可以:分支名称的要点是能够找到一些提示提交,这只有在您计划再次使用该提交或添加或两者兼而有之时才有意义。
要使提交M
,Git 必须组合工作。 这种组合工作技巧需要使用两次git diff
,而完成这项工作需要找到两个分支上的一些合适的提交。 在这种特殊情况下,很容易看出提交H
在br1
和br2
上,并且是最合适的提交:Git 称之为两个分支提示提交的合并基础。 所以 Git 现在运行:
git diff --find-renames <hash-of-H> <hash-of-J> # what we changed on br1
git diff --find-renames <hash-of-H> <hash-of-L> # what they changed on br2
然后,合并代码将两组更改组合在一起,将组合的更改应用于提交H
- 这会将它们的更改添加到我们的更改中,或者将我们的更改添加到它们的更改中,具体取决于您希望如何查看它 - 如果一切顺利,git merge
继续使合并提交M
。
M
的特别之处在于它有两个父母。这是M
唯一特别的地方! 它与其他任何提交一样:它具有所有文件的快照,并且具有元数据。 快照包含合并基中由合并更改修改的文件。 元数据像往常一样包含您的姓名和电子邮件地址,像往常一样包含当前日期和时间,以及(这是唯一的特殊部分)两个父哈希 ID 的列表,而不仅仅是一个。
这使得M
找到两个父提交,这就是为什么我们可以删除br2
. 但是我们不必删除br2
. 如果我们愿意,我们可以继续对br2
进行更多提交:
I--J
/
...--G--H M <-- br1
/
K--L---N--O <-- br2 (HEAD)
如果我们现在切换回br1
并再次运行git merge br2
,我们将得到另一个合并提交:
I--J
/
...--G--H M------P <-- br1 (HEAD)
/ /
K--L---N--O <-- br2
两个差异的合并基础将在这次提交L
(这不太明显),两个提示提交将是M
和O
,这是进入新合并提交P
的文件的来源。 最终的结果是 Git 不必单独考虑I-J
或K
的变化,也不必从H
开始:而是从L
开始。 更多的重复工作和合并会产生更多相同的结果:
...--M-----P-----T <-- br1 (HEAD)
/ / /
...--L--N--O--R--S <-- br2
制作T
的合并基础是O
,两个提示提交是P
和S
。
简而言之,这是git merge
:我们将一些共同起点以来的更改(通过检查从提交到提交的连接找到)结合起来,并与两个父级进行新的合并提交,以便如果有下一次合并,下一次合并可以从我们离开的地方开始。
git pull
一旦你正确理解了提交、git fetch
和git merge
,默认git pull
——基本上只是运行git fetch
然后按该顺序git merge
——很简单。 当然,如果没有很多额外的选项和功能,git pull
就不会是现在的样子,但如果您没有使用它们,我们可以停在这里。
我总是建议 Git 新手避免git pull
因为:
- 第二个命令的选择太多,默认默认(合并)并不总是合适的;
- 第二个命令(合并与变基)产生了巨大的差异;和
- 第二个命令经常"失败"。
失败太强了,这就是为什么我把它放在引号里,但git merge
和git rebase
都可以停在中间。 合并比变基要简单得多(我们稍后会看到),所以它很可能是更好的默认值,但无论哪种方式,你都必须知道如何在失败后清理,否则你就遇到了麻烦——而且,不幸的是(由于不好的原因),清理的方法不同,所以你需要知道你正在使用哪一个。 如果你是 Git 的新手,你甚至不知道你正在运行哪个:你的默认值取决于你的配置,如果你和一个团队合作(这很常见),他们可能会要求你设置默认值以使用git rebase
。
如果您运行git fetch
,然后分别运行第二个命令,您将更好地了解自己在做什么,从而更好地了解如何获取帮助。 这其实并不容易——我曾经有一位同事把这称为"硬"——然而......这在某种程度上更容易。 (也许它只是信息量更大,但它似乎确实有帮助。
git rebase
在使用 Git 时,我们经常发现到目前为止我们所做的一些提交是......好吧,他们还可以,但不是我们真正想要的。 不过,我们这里有一个大问题:提交实际上是不可能更改的。 一旦制作完成,它们就镶嵌在石头上。
但是:如果我们可以将一些提交集复制到一些新的和改进的提交中呢? 假设我们有:
...--G--H <-- main
I--J--K <-- feature (HEAD)
我们已经完成了feature
,但我们刚刚注意到I
中有一个错误,所以我们再做一个承诺来修复它:
...--G--H <-- main
I--J--K--L <-- feature (HEAD)
现在,我们L
的唯一原因是修复I
中的错误 . 如果我们一开始没有将错误放入I
,那肯定会很好。我们可以做到这一点。 我们不能更改提交I
,但我们可以从I
和L
中进行一个新的组合提交 - 我们称之为"提交IL
" - 在H
之后:
IL <-- temporary-branch (HEAD)
/
...--G--H <-- main
I--J--K--L <-- feature
我们通过创建一个新的分支名称temporary-branch
来做到这一点,该名称指向提交H
(与main
相同),然后通过任何方式进行新提交(我们稍后会回到"通过任何方式")。 然后我们做另一个新的提交J
,这是J
所做的重复,但应用于IL
。 我们将此新提交称为J'
,因为它与J
非常相似:
IL-J' <-- temporary-branch (HEAD)
/
...--G--H <-- main
I--J--K--L <-- feature
最后,我们将K
复制到新K'
:
IL-J'-K' <-- temporary-branch (HEAD)
/
...--G--H <-- main
I--J--K--L <-- feature
我们的临时分支现在包含我们希望进行的三个提交。
现在,稍等片刻:不久前,我们观察到通过使用分支名称来查找提示提交来查找提交。 每个分支名称只包含一个哈希 ID。 如果我们强制名称feature
选择提交K'
而不是提交L
会发生什么? 我们会得到这个:
IL-J'-K' <-- feature, temporary-branch (HEAD)
/
...--G--H <-- main
I--J--K--L [abandoned]
提交I-J-K-L
仍然存在,但是如果没有可以找到它们的名称,我们将永远不会看到它们。我们现在可以再次将HEAD
附加到feature
并完全删除临时分支名称:
IL-J'-K' <-- feature (HEAD)
/
...--G--H <-- main
I--J--K--L [abandoned]
看起来我们从一开始就做对了一切。
让我们考虑另一种情况,我们使我们的I-J-K
完美提交,但其他人出现了,并为我们想要的main
添加了一个新的L
。 我们更新了自己的main
:
...--G--H--L <-- main
I--J--K <-- feature (HEAD)
现在我们不喜欢I-J-K
的一件事是他们追随H
:如果他们只是在L
之后,他们会很完美。 我们可以使用完全相同的技巧,创建一个临时分支,复制我们喜欢的三个提交,并使名称feature
指向最终的提示提交:
I'-J'-K' <-- feature (HEAD)
/
...--G--H--L <-- main
I--J--K [abandoned]
废弃的提交从视图中消失了(尽管它们仍然存在于存储库中),似乎我们在L
存在之后就开始了我们的工作。
在这两种情况下,我们所做的都相对简单,尽管它有很多后果:我们正在复制- 并在我们进行一些更改或更改之前,在它们被固定之前 - 一些现有的提交到某种新的和改进的提交。 Git 为此提供了一个主要的"电动工具"命令,即git rebase
。
最基本的git rebase
形式是通过不更改每个提交更改的内容来工作的。 例如,我们将这个用于简单的I-J-K
-on-H
变成I'-J'-K'
-on-L
操作。 每个"复制一次提交"步骤都是通过一个简单的git cherry-pick
命令完成的。3
但是有一个问题:樱桃采摘实际上无法复制合并提交。 如果您尝试这样做,则会收到一条错误消息,告诉您必须提供-m
选项。 此选项告诉git cherry-pick
假装提交是普通的单父提交,而不是合并。四此问题的原始git rebase
解决方案是完全忽略合并提交。
也就是说,如果我们有:
I--J--M <-- feature (HEAD)
/ /
...--G--H--K--L <-- main
我们运行git rebase main
,git rebase
完全忽略提交M
。 它复制只是提交I-J
,将副本放在L
之后:
I--J--M [abandoned]
/ /
...--G--H--K--L <-- main
I'-J' <-- feature (HEAD)
事实证明,这通常是我们想要的。
当它不是我们想要的时,我们有Git-2.18 中的新git rebase --rebase-merges
选项。 这根本不会尝试复制合并。 相反,它会复制导致合并的提交,然后重新运行git merge
以进行新的合并提交,然后根据需要继续复制更多提交。 但是由于我们想删除合并,我们可以忽略这个新奇的选项。
我们还有git rebase --interactive
,它提供了执行挤压和修复操作以及移动提交的能力。git rebase --interactive
指令表中的每个pick
命令对应于单个git cherry-pick
命令。 将 1 更改为squash
意味着以前的操作应该执行一个樱桃选择但尚未提交,而不是执行正常的挑选和提交,然后应该添加此提交,然后我们将提交。
所有这些花哨的选项都只是:花哨的选项增加了基本思想,我们将复制一些提交。 因此,从根本上说,git rebase
需要知道两件事:
- 您想复制哪些提交(然后放弃原始版本)?
- 您想将这些副本放在哪里?
默认情况下,git rebase
命令巧妙地将两个问题合并为一个问题。 我们从观察开始,当我们运行git merge
时,我们总是合并到当前分支中。 同样,当我们运行git rebase
时,我们总是从当前分支复制提交。5仅仅说"好吧,我们想要当前分支上的提交"的问题在于,当前分支上可能有数百甚至数千个提交。 当前分支一直回到第一次提交! 我们不想复制所有这些提交。 我们希望复制这些提交的选定子集。
例如,在上面的例子中,我们要复制的选定提交子集是I-J-M
,除了M
是合并,所以我们毕竟不想复制它。 这样就剩下I-J
作为要复制的提交。
Git 有一个时髦的语法,X..Y
,这意味着所有提交都可以从Y
访问,但不能从X
访问。 这个"可到达来源"的概念是你应该知道的关于 Git 的另一件事:参见,例如,Think Like (a) Git。 在我们的例子中,main..feature
生成完全正确的提交列表:I
、J
,然后是M
。6
由于我们总是要从当前分支复制提交,因此git rebase
不需要您写下名称feature
。 它只需要您键入名称main
,以便它可以自行构造main..feature
并获取要复制的提交列表。而且- 这是聪明的部分 -名称main
也选择了正确的位置来放置副本!我们希望I'-J'
在提交L
之后,并且名称main
选择提交L
。
所以我们只是运行:
git rebase main
或者也许git rebase -i main
获得交互式多样性,Git 知道:
- 哪个承诺复制:
main..feature
,和 - 将副本指向何处:在
main
命名的提交之后
这就是我们所需要的!
有时这不太有效。 对于这些情况,git rebase
--onto
. 我们运行:
git rebase --onto <place> <what-not-to-copy>
要复制的提交列表使用*what-not-to-copy..HEAD
来确定不复制的内容。 也就是说,对于X..Y
的X
部分,我们不说--onto
的部分是X
部分。 然后放置副本的位置是--onto
部分,这就是我们运行git rebase
的方式。
3在现代 Git 中也是如此。 在旧版本中,一些变基使用樱桃采摘,有些则不使用,而在肯定的古代 Git 中,所有变基都没有。 显然,樱桃采摘比其他方法效果更好,这就是这里要记住的想法。
4-m
本身的参数是用于此假装动作的父编号,大多数情况下它只是常量1
,原因我们不会介绍,因为我们在这里没有空间。
5git rebase
有一个变体,它首先运行git switch
,切换到其他分支。 您需要知道的是,此git switch
就像您将其编写为单独的命令一样。 然后,在此git switch
工作之后(假设它确实有效),git rebase
使用当前分支工作。 当整个事情完成后,你最终会跳到你切换到的树枝上,就像你自己跑git switch
一样。
由于这种"假装你先运行了一个单独的命令"方面,我建议避免这种情况,至少在你真正熟悉其他所有内容之前。 它有点类似于git pull
:如果您没有意识到这与运行单独的命令相同,您可能会惊讶地发现git switch br2 && git rebase --onto x y z
让您在分支z
,而不是br2
。 你的git switch br2
有点毫无意义,因为这相当于git switch br2 && git switch z && git rebase --onto x y
.
6实际上,就像 Git 中的其他所有内容一样,它以错误的顺序生成它们:向后。 对于挑选樱桃,我们需要它们进入错误的 Git、转发、顺序。 因此,变基代码在内部使用--reverse
。 它也使用--topo-order
和--no-merges
,尽管出于篇幅原因,我们不会讨论这些细节。
何时不使用git rebase
有些时候你不应该使用git rebase
。 这些主要归结为这样一个事实,即变基是通过复制来工作的。 请记住,git fetch
本身也可以通过复制提交来工作,而git clone
通过复制提交来工作。 我们有我们的分支名称,它们(其他一些 Git 存储库)也有它们的分支名称,我们复制一些提交并调整分支名称以查找新副本。
我们不能强迫其他人调整他们的分支名称以指向新复制的提交而不是原始提交。 我们可以用git push
(礼貌地)问他们,我们可以用git push --force
命令他们,但他们甚至可以拒绝强有力的git push
. 而且,即使我们让另一个 Git 存储库切换其分支名称以指向新的和改进的提交,如果还有其他克隆仍然具有旧的和糟糕的提交,并将哈希 ID 存储在分支名称中怎么办?
因此,如果其他人正在使用旧的提交,我们通常不应该使用 rebase 来改进提交。我们可以做到,如果我们得到所有"其他人"的预先批准,那也没问题。 但是,如果我们变基我们从未让其他人看到的提交,我们保证是安全的,因为在这种情况下,没有其他人使用旧的提交。
一般来说,如果我们没有使用git push
将这些提交发送给其他人,那么重新设置我们自己的分支名称和提交是"安全的"。 即使我们以这种方式使用git push
,如果我们确定没有其他人依赖旧的提交哈希 ID,它仍然是安全的。 如果我们提前与其他人做出安排,如果有人确实依赖这些,那甚至是安全的。
其余的只是(仅仅是?)管理:跟踪谁有哪些提交。 遗憾的是,Git 在这方面做得不好,但 Git 关于提交永久不可变的想法关闭了很多可能性。 (我认为 Mercurial 的"进化"扩展利用了一些可变位卡在他们的提交中:他们定义了一种提交格式,其中哈希 ID 不会覆盖提交的每一位。 这里还有其他一些分布式数据库技术,但 Git 也不使用这些技术。
默认在git pull
上变基
git config --global pull.rebase true
或根据特定git pull
重新定位
git pull --rebase
使用 git fetch
获取远程状态
git fetch
然后如果你想变基
git rebase origin/main
或 如果要合并
git merge origin/main
如果您进行了您不想做的合并 取消合并提交及其带来的修改:
git reset --hard HEAD~1
然后变基
git rebase origin/main