为什么我不能合并这些远程分支?

Why can't I merge these remote branches?

我正在学习 git 并尝试将我的 origin/dev 分支合并到我的 origin/master 分支中,因为 origin/dev 有最新的更新:

我正在输入:

git pull origin dev:temp
git push origin temp:master

但我收到这条消息

To bitbucket.org:fakename/app-ios.git
 ! [rejected]        temp -> master (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org:fakename/app-ios.git'
hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. Check out this branch and integrate the remote changes
hint: (e.g. 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

这是我当前的图表(此帖子的提交代码是假的)

*   23f6a52 (origin/master, origin/HEAD) finalizing merge
|\  
| * gab012e (HEAD -> dev, origin/dev, temp) added .gitignore to dev branch

我希望 origin/master 拥有 origin/dev 的数据,但不确定为什么它没有合并?

我已经备份了所有内容...

谢谢!

git checkout temp
git pull -r origin master
git push temp

应该可以解决问题。

TL;DR

您在评论中提到了 xcuserdatad/UserInterfaceState.xcuserstate 的问题。不应提交此文件。您将要删除该文件并进行新的提交以忽略它。您是否要返回并用 new-and-improved 提交替换所有 existing 提交 also omit 文件已启动给你。这个过程可能会很痛苦,但这样做有其自身的痛苦。

(注意:我不使用 Xcode 并且我不确定如果 Git 从您的 中删除文件会有多大问题工作树,虽然我怀疑 Xcode 只会 re-create 它,即,不是那么糟糕。我将“不要提交它”基于其他答案,例如 this one。)

这个:

*   23f6a52 (origin/master, origin/HEAD) finalizing merge
|\  
| * gab012e (HEAD -> dev, origin/dev, temp) added .gitignore to dev branch

表明已经有一个合并提交——即 23f6a52——合并了提交 gab012e(那个似乎是假哈希,因为真正的哈希 ID 表示为 hexadecimal数字)与其他一些提交,我们看不到其哈希 ID,因为它位于屏幕底部。我在猜测,但也许这就是您想要进行的合并,已经进行了。

您自己的存储库通过名称 origin/master 调用提交 23f6a52,这是您的 Git 对 originmaster 分支的记忆。假设这是最新的,那肯定可以解释这里的错误:

To bitbucket.org:fakename/app-ios.git
 ! [rejected]        temp -> master (non-fast-forward)

你的 git push 打电话给 bitbucket 的 Git 并询问他们是否知道提交 gab012e。他们会有,所以他们会说 我已经有了那个 并且 git push 的第一部分已经完成。其余部分包括一个礼貌的请求:请,如果可以,请将您的 master 设置为指向提交哈希 ID gab012e

他们说:但那不行。如果我这样做,我将失去我的承诺,23f6a52,在我的 master. 的末尾(他们并没有字面上这么说,他们只是说 不,它不是 fast-forward 所以它不正常 但这就是 的意思。)

I want origin/master to have the data of origin/dev

为此,您可能必须删除 此合并。

but not sure why it isn't merging?

它(apparently)已经合并,通过提交23f6a52

(请注意 23f6a52 中的快照不需要 匹配 gab012e 中的快照 ,因为合并并不意味着“相同”。它不是从您在此处显示的内容可以判断出这两个快照中的内容。也无法判断这与您自己的 master 有何关系,如果有的话。)

不该做什么

I am learning git and trying to merge my origin/dev branch into my origin/master branch ...

git pull origin dev:temp

运行使用此命令可能不是一个好主意,尤其是如果您仍在学习 Git。这是一个非常复杂的组合。

不是每个人都同意我的看法,但我建议新手避免git pull。它所做的是 运行 两个 Git 命令:

  • 首先,git pull 运行s git fetch。几乎你传递给 git pull 的每个参数都会直接传递给 git fetch,在这种情况下,git pull origin dev:temp 运行s git fetch origin dev:temp。这是一个复杂的命令,因为它使用了一个完整的 refspec,dev:temp,指示 Git 创建或 fast-forward 你自己的(本地)分支 temp 基于获取 origindev.

    的结果
  • 然后,假设 git fetch 成功,git pull 继续 运行 第二个命令。第二条命令的选择本身就有些复杂。如果您从未配置过任何其他内容,则第二个命令(通常)是 git merge。如果你配置了 pull to 运行 rebase,它通常是 git rebase。您可以在 运行 git pull 时覆盖这些(尽管您没有,所以它可能使用 git merge,最终可能成为 no-op)。

这些不同的步骤中的每一个都可能有些复杂。在某些情况下很难预测结果,您 观察 的结果可能具有误导性。如果您还是初学者,我认为最好将各个命令分开运行,然后观察它们的作用。这使您能够更好地理解您可能需要哪个第二个命令(如果有的话!),它们的作用,以及何时使用每个.

做什么

git fetch origin 开始(或简称 git fetch,默认为 origin)。1 这个:

  • 使用短名称 origin 查找 URL;
  • 使用生成的 URL 调用其他一些 Git;
  • 询问其他人 Git 他们 有哪些分支(和其他名称),以及这些名称对应的提交;
  • 带来他们拥有的、您没有的、您的 Git 想要的任何提交——通常是“所有这些”;2 最后
  • 更新你的remote-tracking名字,比如origin/masterorigin/dev,基于他们[=491]的提交=] 名称标识。

(有限的获取,如 git fetch origin dev,仅更新 origin/dev,因为它不会费心从他们的 master 获取任何新提交。

现在你有了他们的提交,看看你有没有他们没有的提交,你希望他们现在拥有。例如,如果你的 dev 严格领先于他们的 dev——比如说,你有两个他们没有的新提交,而他们没有你没有的提交——你想要他们保存这两个提交,以防您的笔记本电脑在您快速休息时突然起火,3 您可以使用 git push 发送这些提交。请参阅下面关于 git push 的注释。如果您愿意等待,或者您的 Git 既领先 又落后 他们的 Git,请参阅下面的合并说明。

(您已经知道如何使用 git log --all --decorate --oneline --graph 或类似的方式查看哪些提交。)

git fetch的真正好处是你可以随时做。它所做的就是获得任何new 从其他 Git 提交,然后更新您自己的 remote-tracking (origin/*) 名称。它不会影响您正在做的任何事情——好吧,只要您不使用 dev:temp 样式语法即可。


1实际默认是当前分支的远程,如果它有的话。如果不是,则默认为文字字符串 origin。如果您只有一个遥控器,名为 originorigin 将是正确的,在更简单的设置中,您将只有一个遥控器。

2这里的例外是 single-branch 克隆和其他一些特殊情况。另外,如果你使用 git fetch <em>remote somebranch</em>,例如,你的 Git 只会麻烦带来新的提交它需要更新 <em>remote</em>/<em>somebranch</em>。这意味着稍后的 git fetch 将需要带来此 git fetch 跳过的任何提交。如果您的 Internet 连接速度较慢,有时这是值得的,但通常最好一次将所有内容都带过来,因为这样压缩效果更好:如果 git fetch xyz br1 需要 10 分钟,而 git fetch xyz br2 也需要 10分钟,git fetch xyz 可能 需要 20 分钟,而更像是 12 到 15 分钟。这些数字是当场编造的(比如所有统计数据的 37.12%),但总的想法是正确的:如果你分期付款,随着时间的推移,你最终支付的费用会比你只预先支付全部费用要多。

3参见,例如,https://abcnews.go.com/WNT/video/charging-laptop-bursts-flames-moment-caught-home-camera-45312573


如果你完全落后,考虑 fast-forward 合并

正如您可能已经收集到的那样:

  • 每个 Git 提交都有一个哈希 ID。此哈希 ID 唯一标识一个特定的提交。此 ID 在 每个 Git 存储库中都是唯一的,并且 Git 保证每个 Git 都会同意 that commit 获取 that ID,通过将哈希 ID 计算为提交内容的加密校验和。

  • 每个 Git 提交都有 Git 知道的所有文件的完整快照。

  • 每个提交也有一些元数据,即关于提交的信息。这包括提交人(“作者”)的姓名和电子邮件地址,以及 date-and-time-stamp。它包括其他信息,例如日志消息。而且——对于 Git 至关重要——它包括一个 previousparent 提交的列表。 parent 链接是您在 git log --graph 输出中看到的提交图的形式。

如果你自己的 dev 落后于 origin/dev 而不是 领先于 origin/dev 并且没有分歧 - 那表示有人在 origin 上向 dev 发送了新的提交。如果您希望这些提交出现在您自己的 dev 中,现在是时候了:

git checkout dev
git merge origin/dev

或更简单的:

git checkout dev && git merge

第二个版本假定 origin/dev 设置为 devupstreamgit merge 命令将默认使用 upstream , 如果你没有在这里指定一些东西。我实际上喜欢在这里添加 --ff-only 标志,因为我只希望合并在 可以 作为 fast-forward 完成时成功。 (我有一个别名,git mff,那个 运行s git merge --ff-only,这就是我使用的。)这让我有点粗心/漫不经心,只是 运行 命令没有首先检查它是否 执行 fast-forward.

如果你同时领先落后,你通常会想要合并或变基

如果您有他们没有的新提交,他们有您没有的新提交,您可能需要将您的工作与他们的工作结合起来。这是 git pull 运行s 出现的第二个命令:

  • git merge 将进行完整的 three-way 合并(不仅仅是 fast-forward)。
  • git rebase 将获取您现有的提交并将它们复制到 new-and-supposedly-improved 提交。如果复制全部有效,则 rebase 将放弃旧的(和糟糕的?)提​​交对于新的(和改进的?)。

变基比合并复杂得多,因为它在 commit-by-commit 基础上工作。它使用 git cherry-pick 或相当接近等效的东西,4 用于 each 需要复制的提交。 5 这些 cherry-pick 操作中的每一个都可以在合并冲突的中间停止,因此如果 git rebase 需要复制 5 次提交,则有 5 次机会停止。合并只有一次机会因合并冲突而在中间停止。 (另一方面,一个冲突可能很大,而 5 个冲突较小。)

这里是合并还是变基由您选择。不过,我认为关于 git pull 的其中一件事是你必须 做出 这个选择 你看到是否合并将是 fast-forward 或真正的合并。最近,Git 增加了使 git pull 只进行合并的选项 如果 是 fast-forward 合并。我声称这是正确的默认值(但它不是当前的默认值,所以我仍然建议避免 git pull)。


4现代 Git 使用 cherry-pick 但内置了变基。 Git 的旧版本有时使用 cherry-pick,有时使用 git am,rebase 是一个 shell 脚本。您可以通过添加 -m and/or -s 参数或进行交互式变基,使 Git 的那些版本使用变基; interactive rebase literally 运行s git cherry-pick.

5复制的提交集是......复杂的,尤其是在现代Git中带有--fork-point选项。然而,在正常的简单情况下,这是您拥有的每个提交,而他们没有。


Full-blown合并

要进行 full-blown three-way 合并,我建议您使用(本地)分支,例如 devmaster(或 main 如果你从 master 切换到 main):

  • 运行 git checkoutgit switch(在 Git 2.23 或更高版本中)以及要用于新合并提交的分支名称.这让你“在”那个分支上,因为 git status 现在会说 on branch dev 或其他什么。

  • 运行 git 合并 <em>name</em> or git merge <em>commit-hash-ID</em> 开始合并过程。在此处使用名称,如 git checkout master && git merge dev,设置默认提交消息以使用分支名称(例如,merge branch dev)。除此之外,哈希 ID 与名称一样好。哈希 ID 很难输入,因此您可能希望使用该名称。您应该意识到 Git 在这里 不需要 名称。它需要一个哈希ID,它只是将名称转换为一个哈希ID来进行实际的合并。

合并本身通过找到一个 shared 提交来工作。假设我们有一个这样的提交图:

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

表示 master 上有两个不在 dev 上的提交,反之亦然。在本例中,两个 分支上的最佳提交是提交H。 (请记住,后来的提交向右,较早的提交向左,并且 Git 从两个 end-point 提交开始并向后工作。因此提交 H 是第一个 shared 提交,在两个分支上,因此是 best 共享提交。)

合并过程现在通过将提交 H 中的快照与两个选定提交中的两个快照中的每一个进行比较来工作:

git diff --find-renames <hash-of-H> <hash-of-J>   # what "we" did on master
git diff --find-renames <hash-of-H> <hash-of-L>   # what "they" did on dev

Merge 通过组合这两组更改来工作,将组合的更改应用到提交中的快照 H——这在添加“他们的”的同时保留“我们的”更改——并根据结果进行新的提交.或者,如果合并出错,合并会在中间停止,在您的工作树和 Git 的索引中留下一团糟,您的工作变成修复混乱并继续合并。

假设一切顺利,Git 进行新的 merge 提交,这是一个包含两个 parent 的提交。6 我们可以这样画:

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

请注意,如果提交 Jorigin/master 的提示(现在仍然是),您现在可以 git push origin master 将提交 M 发送到 origin.此提交将严格领先于他们自己的 master——就他们而言,快进——因此他们很可能会接受此推送。


6从技术上讲,合并提交是具有两个 或更多 parent 的任何提交,但您无能为力处理超过两个你不能用额外合并做的事情。事实上,这有点相反:有些事情你 不能 使用 so-called 章鱼合并,有 3 个或更多 parents,您 可以 进行多个 two-parent 合并。这是它们的主要价值:如果您遇到这种情况,您可能会认为它是这些更简单的合并之一。一个真正的 Git 绝地大师(或西斯尊主)可能会做出违反这些期望的事情,虽然.


git push

一旦你有一个本地分支,与 origin(或其他一些远程 Git 上的相同分支名称相比,你可以 运行 git push 将新提交发送到他们的 Git。与 git fetch 一样,这首先调用他们的 Git——但这次你是发送者,而不是接收者,并且你的 Git 列出了你的某些提交的哈希 ID选择.

在上面的示例中,我们刚刚进行了新提交 M,因此这是我们唯一拥有而他们没有的提交,当我们 运行 git push origin master—请注意,我们应该在这里使用 所有四个词,并且不要遗漏 originmaster7——我们的 Git 将提供他们的 Git 新提交 M,他们将接受,然后提供 M 的两个 parent,他们将拒绝 不用了,我已经有了。然后我们将打包提交 M,包括它的文件,并将包发送过来。然后我们的Git会礼貌的向他们的Git请教,请,如果可以的话,把你的master设置为_____,填在空白处使用提交的哈希 ID M.

他们现在将检查此操作对他们来说是否是 fast-forward。 如果是,他们将接受请求,8 现在他们的 master 将提交 M。我们的也会,因为我们在制作时将它放在 master 上。


7如果你 运行 git push 没有参数,Git 以同样的方式填写 origin 部分不带参数为 git fetch 做。然后它根据 push.default 填写 分支名称 部分。 Git版本2(所有版本2版本)中的push.default设置为simple,即使用当前分支。但是,Git 版本 1 中的默认设置是 matching。这有时是您想要的,但对于新手来说很少如此,这就是为什么他们在 Git 2.x.

中更改了它

我自己实际上倾向于使用 git push origin HEADHEAD 转换为 当前分支 。在 GitHub 上进行 pull requests 的特殊情况下,我有时会使用 git push origin HEAD:<em>new branch name 我当场编的</em>。然后我重命名本地分支,在输入 git push 命令时弄清楚了我 想要 为本地分支使用什么名称,但没有更早。 :-)

8根据站点的不同,存储库所有者通常可以添加各种规则来保护特定的分支名称,以便只有列入白名单的用户才能直接向他们推送。在这种情况下,您可能需要将自己加入白名单。


删除并忽略不应该提交的文件

重要的是要记住,在 Git 中,提交Git 知道的所有文件的快照 在您(或任何人)制作该快照时。一旦创建,该快照将无法更改。事实上,任何现有提交都无法更改。这是由于 Git 使用的散列方案。

由于每个提交都以计算机其余部分无法使用的形式存储,因此每个提交都必须提取到普通的日常文件中。当 Git 执行此操作时,它会获取 提交的 文件,将它们扩展为可用形式,并将它们粘贴到工作区中。 Git 将此工作区称为您的 工作树

由于您的工作树是一个充满文件的普通目录(或者如果您喜欢这些术语,则为一组“文件和文件夹”),您现在可以在此处创建 文件。 Git 不会知道他们。 Git 它知道的文件列表保存在 Git 称为它的 index.

索引不只是 列出 文件。它实际上以 pre-Git-ified 形式保存了其中的 快照 :压缩和 de-duplicated,准备进入 next 提交.当您使用 git add 时,您是在告诉 Git:将此文件复制到您的索引中。 如果之前有一个文件,它会被踢出(即,不是 overwritten 因为它们是存档形式),而是新文件进入。如果文件 之前 Git 的索引中没有 ,那么现在它连同它的名称。

当您 运行 git commit 时,Git 保存 其索引 中的那些文件,格式为 然后 索引中。这成为新的快照。因此 Git 的索引始终是您建议的 next 快照。9 如果您避免将文件 添加到 Git 的索引,它不会进入提交。

每次更改文件时都手动添加每个文件,这既烦人又痛苦。于是就有了一个en-masse add everything操作(其实是多次这样的操作)。但是如果你这样做,这会添加你可能永远不需要的文件提交。

为了防止那个,Git有.gitignore。这个文件的名字有点不对。它不会忽略 已经在Git 的索引中的文件。它主要做的事情是抑制来自git status投诉,关于未跟踪文件Git提醒您添加。该文件也许应该命名为 .git-do-not-complain-about-these-untracked-files。但是,它也会阻止那些 en-masse add a bunch of files 操作添加那些未跟踪的文件。所以更好的名字是.git-do-not-complain-about-these-untracked-files-and-do-not-auto-add-them-during-an-en-masse-add-operation。这仍然不完全准确,但比 .gitignore 更接近。但这也很荒谬,所以.gitignore它是。

这里的关键是 .gitignore 中列出一个文件不会导致它被忽略 。提交 文件的缺失导致它被忽略。当文件不在 Git 的索引中时,它不会在您的 next 提交中。但是如果它在某个 existing 提交中,并且您检查了该提交,Git 会将提交的文件 复制到 它的索引,然后将该文件(并将其展开为有用的形式)复制到您的工作树。

要获取已经在 Git 的索引 out 的 Git 的索引中的文件,我们使用 git rm

git rm file-that-should-not-have-been-committed

这会删除 索引副本 工作树副本。为避免立即删除工作树副本,我们可以使用:

git rm --cached file-that-should-not-have-been-committed

仅删除 index 副本,单独留下工作树副本。然后我们可以进行新的提交,新的提交将缺少文件。

但是:没有 existing 提交可以更改。因此 old 提交将 拥有 文件。旧提交和新提交之间的区别会说删除文件。对旧提交的任何使用都会为您提供文件的旧副本。任何切换旧提交,一个缺少文件的新提交,删除文件.

这就是为什么删除一开始就不应该提交的文件会很痛苦的原因。

可以获取某个存储库中的所有提交并将其中的 所有 复制到缺少某些文件的 new-and-improved 提交,但结果是new repository 需要丢弃旧存储库的所有副本。你 可以 这样做,尤其是在某些项目开始的时候,如果你知道每个人都有旧的提交并且可以说服他们全部切换。


9冲突合并中出现异常。在这里,Git 的索引被扩展了,您实际上 不能 从中创建快照,直到您通过解决冲突将其缩小回其正常形式。