【问题标题】:Git branch creation is giving rise to merge conflictGit分支创建引起合并冲突
【发布时间】:2020-04-18 21:34:21
【问题描述】:

我有一个 shell 脚本,我想在其中创建一个本地分支,提交一些更改并将其推送到上游。稍后我需要在 TFS 中创建一个拉取请求。脚本如下:

# enter the directory containing the repo
echo "....Entering $dest"
cd ~/IdeaProjects/$dest

# get the latest development branch
echo "....git pull"
git pull

# checkout development so that we can create a branch from the development branch
echo "....git checkout development"
git checkout development

# now create a branch
echo "....git checkout -b $branch"
git checkout -b $branch

# push to upstream to sync with it
echo "....git push origin"
git push origin $branch

# make some changes
# ...

# add changed / added files
echo "....git add"
git add .

# now commit the changes
echo "....git commit"
git commit -m "modified domain object $fileList"

# push to upstream
echo "....git push"
git push --set-upstream origin $branch

到目前为止一切顺利。唯一的问题是,在创建拉取请求时,有时会与开发发生合并冲突。有时它甚至会立即发生(即在执行脚本之后),并且可以肯定没有其他人修改 repo 代码库的任何部分。即使仅添加了一个全新的类或仅添加了一个字段,也会发生这种情况,例如。

class ExistingPojo {
    // fields

    // add this new field
    NewPojo embedded;
}

// an new class added
class NewPojo {
    // fields
}

问题是当它失败时我很难理解模式。这没有多大意义,因为如果我从开发中创建一个分支,那么我应该将 master 中的最新代码放入新分支;修改后不应该有任何冲突,因为没有其他人进行任何更改。

【问题讨论】:

  • 你提到你有时会出现合并冲突,两个分支有什么区别?

标签: git tfs


【解决方案1】:

TL;DR

您可能想要一个执行以下操作的脚本:

#! /bin/sh

# ... check actual invocation and set some variables:
branch=<something>

git fetch origin || exit

# check actual invocation and/or fetch results and set one
# more variable:
startpoint=<something>   # or origin/something
# you may even want to use `git rev-parse` here to convert
# a name to a raw hash ID:
# startpoint=$(git rev-parse $startpoint) || exit

git checkout -b $branch $startpoint || exit
git push -u origin $branch

startpoint 将取决于您想要开始工作的位置(例如,基于哪个提交)。您可能希望在此处添加任何git pull 步骤。

如果您在分支方面考虑太多,这可以解释这一点:

问题是当它失败时我很难理解模式。

这里的问题是 branch 在 Git 中并不真正意味着任何东西。有时,当人们说一个分支时,他们的意思是一个分支名称。其他时候,它们表示一系列提交,即提交 DAG 的某些部分。 (另见What exactly do we mean by "branch"?)问题是分支名称会随着时间推移而变化,这使得依赖它们来描述一系列提交既非常有用又非常棘手。

作为Hugo Valenza M suggests,我一般也建议不要使用git pullgit pull 所做的是运行两个 Git 命令:

  • 首先,它运行git fetch,将您提供给git pull 的大部分其他参数传递给(如果有的话)。这一步让您的 Git 调用其他 Git 并获得新的提交——提交其他 Git 拥有的、您尚未提供的、他们提供的提交,而您的 Git 决定它应该接受。

    你的 Git 通常会记住 他们的 Git 的分支名称,在这个过程中,使用 Git 所谓的 remote。遥控器主要是记住 URL 的短名称,但拥有它可以启用许多其他功能。在您的 Git 存储库中只有一个名为 origin 的遥控器是很常见的。

    通常,在您的 Git 调用 origin 的 Git 后,您的 Git 现在拥有自己的他们分支名称的记忆:他们的 master 现在是您的 origin/master,他们的 @987654336 @ 现在是你的origin/develop,以此类推。这些名称(以origin/ 为前缀的名称)是您的 Git 的远程跟踪名称。它们与常规(本地)分支名称略有不同,有时甚至明显不同。

  • 然后,git fetch 成功后,git pull 运行第二个 Git 命令。通常的默认命令是git mergegit merge 是您可以 获得合并冲突的地方。您可以指示 Git 改用git rebase;在这里,您可以遇到合并冲突。您不会总是遇到合并冲突!这取决于您的提交图以及各种提交的内容。

如果您自己运行这两个命令,您将获得更直接的控制。最重要的是,您可以在继续之前检查git fetch 命令的结果。您还将立即看到 git mergegit rebase 步骤使用您的当前分支运行。1git fetch 步骤没有,2 这意味着您可以随时运行git fetch,无论您之前使用过什么git checkout,但合并或变基步骤确实如此,所以这很重要。

在您的特定情况下,您想要的第二个命令可能 既不 git merge 也不 git rebase。所以在这里,您可能想要git fetch 后跟... 好吧,我们稍后会谈到,但您已经在上面看到它可能是git checkout -b


1好吧,当您完全了解合并和变基时,您就会知道。因为git pull使用它们,你必须理解它们;使用git pull 是无法解决这个问题的。

2请注意,git fetch 可能会使用当前分支来确定要使用的远程,如果您定义了多个远程。


创建新分支的注意事项

在 Git 中,分支名称(例如 masterdevelopfeature/shortbug/tall 等本地名称)仅包含一 (1) 个提交的哈希 ID。

要创建一个 分支,您需要在存储库中选择一个现有提交(任何提交都可以)并告诉 Git:创建一个新名称来保存该提交的哈希 ID。 您可以使用git checkout -bgit branch 来执行此操作:

  • git branch <em>name</em> <em>commit-hash</em> 将创建新名称 name,使其包含 commit-hash。如果省略commit-hash,则默认为HEAD

  • git checkout -b <em>name</em> <em>commit-hash</em> 将作为单个事务创建新名称,git checkout 创建名称(以及给定的提交,通过其哈希 ID)。与git branch 一样,默认提交哈希是通过解析特殊名称HEAD 获得的。 (Transaction 这里的意思是,如果这两个部分中的任何一个失败,则整个命令什么都不做:新名称 not 创建并且您没有更改哪个分支和/或提交您已签出。)

还有许多其他方法可以创建本地分支名称——Git 是一个大工具箱,有很多工具,其中一些做的事情太多。 (这就是 Git 2.23 引入 git switchgit restore 的原因:它们将 git checkout 可以做的部分分解为两个单独的命令,这提供了一些减少混乱的希望。)但是让我们从这两个开始。请注意,它们有相同的想法:您选择一些提交(可能通过哈希 ID,或者可能只是您现在签出的提交)并创建一个包含该特定哈希 ID 的新名称。

分支如何在 Git 中工作

其中大部分的关键在于 Git 与 分支 无关。这真的是关于提交。分支——或者更准确地说,分支names——只是查找提交的一种方式。

在 Git 中,每个提交都是一个永久的——或者大部分是永久的3——并且是只读的(100% 完全只读)单元:

  • 保存所有文件的快照(Git 有时将其称为 ),
  • 连同一些元数据:有关提交的信息,例如提交人、提交时间。

此数据(树)和元数据集合具有唯一的哈希 ID。该哈希 ID 是一大串难看的字母和数字,4 看起来完全随机,人类无法记住。但我们不需要记住它们:毕竟,这就是我们的计算机的用途。

这是分支名称的来源。对于我们和 Git 来说,分支名称会记住被视为分支一部分的 last 提交的哈希 ID。但是之前的提交呢?这就是 Git 的巧妙技巧之一。每个提交都会记住其之前的直接提交的哈希 ID。大多数提交只记住一个哈希 ID。这一个哈希 ID 是提交的 parent,即在此之前 的提交。

这意味着,给定链中的 last 提交,通过分支名称找到,Git 可以在该链中找到 earlier 提交。如果我们让单个大写字母代表哈希 ID——尽管我们会在几次提交后用完——我们可以这样绘制:

... <-F <-G <-H   <--master

在这里,名称 master 包含一些哈希 ID H。 Git 使用它来查找提交H。提交H 本身在其元数据中包含提交G 的哈希ID。 Git 使用它来查找提交 G,其中包含提交 F 的哈希 ID。这一直重复,直到 Git 完成了第一次提交,因为它不能指向任何更早的提交,所以它不指向任何更早的提交。此处跟随箭头的动作停止。

因此,给定链中 last 提交的哈希 ID(例如来自分支名称的哈希 ID),Git 可以找到包含在该分支中的所有提交。要将提交添加到分支,我们让 Git 通过分支名称检查最后一次提交,并记住 我们正在使用哪个分支名称:

...--F--G--H   <-- master (HEAD), develop

然后我们像往常一样创建一个新的提交(编辑文件,git addgit commit)。 Git 打包一个新快照,添加元数据,将新提交的父级设置为提交H,并写出新提交。这会生成一个新的、唯一的、又大又丑的哈希 ID I,我们可以这样绘制:

             I
            /
...--F--G--H   <-- master (HEAD), develop

现在第二个鬼鬼祟祟的 Git 技巧发生了:Git 将新的哈希 ID 写入附加了特殊名称 HEAD 的分支名称。 结果是:

             I   <-- master (HEAD)
            /
...--F--G--H   <-- develop

请注意,通过H 的提交现在在两个 分支上,而提交I 仅在master 上。再次提交Jmaster 现在指向J,它又指向I,又指向Hdevelop 的名称没有改变:它仍然指向现有的提交 H

如果我们现在git checkout develop,Git 再次提取提交H 以使用,并将特殊名称HEAD 附加到develop

             I--J   <-- master
            /
...--F--G--H   <-- develop (HEAD)

如果我们现在进行一些新的提交 KL,这一次将更新名称 develop

             I--J   <-- master
            /
...--F--G--H
            \
             K--L   <-- develop (HEAD)

在这里我们有分支,因为很多人都指他们:不同的发展。但是每个分支name 只标识一个提交。我们这里指的分支是图片段:提交...-F-G-H-I-J...-F-G-H-K-L

在 Git 中,能够停止向后移动通常很重要。我们可能希望列出在master 上的提交,而 不是develop 上的提交,和/或反之亦然,以便我们可以单独看到 I-J 和单独看到 K-L .但同样重要的是能够看到 graph 并看到这两个分支(或图的子集)在提交 H 时出现分歧。 (当像 Git 那样向后工作时,它们con-verge 在提交 H。)


3任何 Git 提交的持久性或缺乏性取决于它的可达性。有关可达性的更多信息,请参阅 Think Like (a) Git

4从技术上讲,它是提交内容的哈希。 Git 目前使用 SHA-1,但 Git 人员打算在未来过渡到 SHA-256,这将是非常 interesting


合并、非真正合并和合并冲突

Git 的合并有点复杂,在这里我不会全部介绍,但让我们说明两种不同的情况。假设我们有这个图形片段:

...--o--o--o   <-- branch (HEAD)
            \
             o--o   <-- origin/branch

每一轮o 代表一个提交。您可以让 Git fast-forward 名称为 branch,以便它与名称 origin/branch 指向 相同的提交。也就是说,将origin/branch名称的提交变成分支名称branch的提示提交是没有问题的,像这样:

...--o--o--o
            \
             o--o   <-- branch (HEAD), origin/branch

在此过程中没有提交丢失,因为 Git 现在从最后一次提交开始并向后工作,并且仍然访问(并查看并作为历史记录)链中的每个提交。

但是,当我们有一个更复杂的图表时,比如这个:

             I--J   <-- master
            /
...--F--G--H
            \
             K--L   <-- develop

而我们要git checkout master,以便使用commit J 开始,然后运行git merge develop 以结合工作,现在Git 不能只是打乱一些分支名称。现在,Git 真的必须结合 工作。

在这种情况下,Git 所做的是它首先定位 最佳共享提交,方法是从两个分支提示开始并向后工作。很明显,在这种情况下,最好的共享提交是提交H。这个最佳共享提交是合并操作的合并基础

接下来,Git 在内部运行两个git diff 命令。5 首先,它将提交H 中的快照与当前提交J 中的快照进行比较:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed

然后运行第二个git diff 以查看HL 之间的变化:

git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

现在 Git 结合这两组变化。结果是 Git 应该对H 中的快照进行一组更大的更改。

只要我们所做的更改之一 (H-vs-J) 在相同的文件和相同的行中,就会发生合并冲突 作为他们所做的更改之一 (H-vs-L)。

对于所有其他情况——我们触及了他们没有触及的文件,反之亦然,或者我们触及了他们没有触及的行,反之亦然——Git 可以自行组合更改。对于这些特殊情况,如果我们进行了完全相同的更改,Git 只能自行组合更改。在这种情况下,Git 只获取更改的一个副本,而不是两个副本。

每当您遇到合并冲突时,这意味着您告诉 Git 执行 git merge(或 git rebase 或其他使用合并引擎的命令6),并且您有一个足够复杂的如果 (a) Git 无法通过快进来伪造它,并且 (b) Git 的合并引擎无法自行组合这些更改。

如果没有合并冲突,git merge 继续进行 合并提交,这是一个链接回之前的 HEAD 和另一个提交的提交,就像这样:

             I--J
            /    \
...--F--G--H      M   <-- master (HEAD)
            \    /
             K--L   <-- develop

如果存在冲突,Git 会给你留下一团糟:你必须自己解决冲突,指导 Git 如何为提交 M 制作最终快照,然后使用 git merge --continue (或 git commit,在古代 Git 版本中)手动完成合并。


5在内部,很多情况下它并不一定要实际运行git diff,但是当事情发展到最困难的情况时,或多或少会发生这种情况。 --find-renames 选项尤其需要在被比较的两个提交中的所有文件名的树形视图。

6Rebase 实际上是一系列git cherry-pick 操作,它们实际上是一种合并形式——至少在组合更改方面——所以这些也可能有冲突.事实上,由于 git rebase 可能会复制许多提交,因此您可能会一次又一次地遇到合并冲突,而 git merge 只会让它们发生一次。


为新功能创建新分支时,您很少需要这些

通常,当您要创建一个 功能分支时,您根本不希望进行任何合并。您可能想从git fetch 开始。正如我们之前提到的,这会让你的 Git 调用其他 Git,例如 origin 的 Git,并从中获取他们拥有的任何新提交,而你没有。然后,您的 Git 会创建或更新您的 远程跟踪名称,例如 origin/master,以识别它们(另一个 Git)作为其分支提示的相同最后提交.

然后,假设在您的本地存储库中,您有:

...--G--H   <-- master (HEAD), develop, feature-X

您运行 git fetchgit fetch origin,这会让您的 Git 调用位于 origin 的 Git,并获得新的提交:

          I--J   <-- origin/master
         /
...--G--H   <-- master (HEAD), develop, feature-X
         \
          K   <-- origin/feature-X

您想从一个新的feature-Y 开始。您可能希望从origin/master 上的最新提交开始——提交J——或者如果feature-Y 依赖于feature-X,则可能从origin/feature-X 上的最新提交开始。

你现在应该做的是选择你想要的提交。这不是 任何 您的 分支名称中的一个。

如果您愿意,您可以通过git checkout feature-Xgit merge origin/feature-X 将您自己的feature-X 快进到origin/feature-X

          I--J   <-- origin/master
         /
...--G--H   <-- master, develop
         \
          K   <-- feature-X (HEAD), origin/feature-X

现在你的名字feature-X 和他们的名字feature-X(你的origin/feature-X)标识了同一个提交,K。但是完全删除你的名字feature-X 可能更有意义。让他们的feature-X,你的origin/feature-X,引导你。您根本不打算在 feature-X 上工作,而是在 feature-Y 上工作。7

所以,此时你可以这样做:

git checkout -b feature-Y origin/master

如果你想从提交 I 开始,或者:

git checkout -b feature-Y origin/feature-X

如果你想从提交 K 开始。 -b 选项将使 git checkoutHEAD 附加到新分支名称,该名称将从所选提交开始。


7当然,如果您打算同时处理这两个问题,那么请继续保持/更新您的feature-X


分支机构有上游

这里有一个烦人但有用的怪癖。 Git 中的每个 branch name 都允许(但不是必需)设置一个 upstream。分支的上游名称提供了一些不错的功能。 Git 将自动根据您用作起点提交的内容设置新分支的上游:

  • 如果您使用原始哈希 ID,则新分支的上游是 未设置
  • 如果您使用本地分支名称,默认情况下,新分支的上游是 unset
  • 如果您使用origin/masterorigin/feature-X 之类的远程跟踪名称,默认情况下,新分支的上游就是此远程跟踪名称。

所以:

git checkout -b feature-Y origin/feature-X

将新分支名称feature-Y 的上游设置为origin/feature-X。这几乎肯定不是你想要的。

由于您只是在本地创建了feature-Y,因此可能还没有origin/feature-Y。这意味着 Git 不会让您将这个新分支的上游更改origin/feature-Y ...。所以现在可能是时候运行了:

git push -u origin feature-Y

这个命令让你的 Git 调用他们的 Git,看看你是否有他们的 Git 没有的 feature-Y 的新提交,8 然后让他们设置 他们的 分支名称feature-Y 来识别与你的 分支名称feature-Y 相同的提交。这将在 Git 中 创建 feature-Y origin

origin 上创建feature-Y 的成功将告诉您的Git,您的Git 现在应该记住他们的 feature-Y 指向同一个提交。所以现在你的 Git 有了一个新的远程跟踪名称,origin/feature-Y。您现在可以将分支feature-Y 的上游设置为origin/feature-Y

git push-u 选项告诉git push,在成功在源上创建feature-Y 并因此在本地创建origin/feature-Y 后,您的Git 应该以这种方式设置分支的上游feature-Y。所以这个git push -u 操作一次实现了三件事,都是你想要的。

如果您的 git checkout -b 命令使用原始哈希 ID(例如通过在 origin/whatever 上运行 git rev-parse 获得的 ID),您的初始分支结帐将没有任何上游设置,这没关系。然后,您可以推迟 git push -u 步骤,直到您有新的提交要发送,只要 other 中创建 feature-Y 分支名称是可以的吉特。

如果您的git checkout -b 设置了上游,并且您想推迟在另一个 Git 中创建名称,您可以使用:

git branch --unset-upstream

只是取消设置当前的上游。或者,您可以修改git checkout -b 以添加--no-track,这告诉git checkout -b 在从远程跟踪名称创建本地分支时不要设置上游。


8当然,您(还)没有任何他们没有的提交:您的feature-Y 指向您刚刚从他们那里获得的提交,或者你们都已经共享了。

【讨论】:

    【解决方案2】:

    用这个替换你的第一个 git pull:

    git fetch --all

    【讨论】:

    • 你永远不应该使用git fetch --all。 (好吧,几乎从来没有:如果您可以定义 remote 并知道 --all 表示 所有遥控器,而不是 所有分支,请随意使用它.) 在不使用--all 的情况下使用git fetch 是很合理的,但是在获取之后,您将需要运行更多Git 命令。这就是git pull 存在的原因:它运行git fetch,然后运行第二个命令。 git pull 的缺点是你必须提前选择第二条命令,然后才能看到 git fetch 做了什么。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-04
    • 2015-10-02
    • 1970-01-01
    • 2020-07-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多