Gitlab 从命令行合并合并请求不生成任何提交(无快进)



我正在尝试从命令行手动合并合并请求。

在 Gitlab 的合并窗口中,已经存在一些代码来执行此操作。每当我合并来自 Gitlab 网站的请求时,它都会生成一个合并提交,这被称为 --no-ff提交。但是当我尝试手动执行此操作时,尽管我在命令行中指定了 --no-ff 选项,但它不会生成任何额外的提交。有什么建议吗?我也已经尝试过提交修改选项。

git fetch <Fork_Repo_URL> <Fork_Repo_Branch>
git checkout -b <Branch_Name> FETCH_HEAD
git fetch origin
git checkout origin/master
git merge --no-ff <Remote_Name>-<Branch_Name>
git push origin master    

尝试后

git log --oneline --decorate --all --graph

我正在得到

* dba92a6 (origin/falcondev, central/merge-requests/21, falcondev) check again with log
*   fdc761e (HEAD -> central/falcondev) Merge remote-tracking branch 'central/merge-requests/20' into central/falcondev
|
| * f6c3a9b (central/merge-requests/20, central/falcondev) one more time
|/
* 1e5a6b0 trying with pull
*   d7a3fd9 Merge branch 'sourodeep.c/merge' into central/falcondev
|
| * 67d1ee8 (central/merge-requests/19, sourodeep.c/merge) new no work
|/
*   078a128 Merge remote-tracking branch 'origin/falcondev' into central/falcondev
:...skipping...
* dba92a6 (origin/falcondev, central/merge-requests/21, falcondev) check again with log
*   fdc761e (HEAD -> central/falcondev) Merge remote-tracking branch 'central/merge-requests/20' into central/falcondev
|
| * f6c3a9b (central/merge-requests/20, central/falcondev) one more time
|/
* 1e5a6b0 trying with pull
*   d7a3fd9 Merge branch 'sourodeep.c/merge' into central/falcondev
|
| * 67d1ee8 (central/merge-requests/19, sourodeep.c/merge) new no work
|/
*   078a128 Merge remote-tracking branch 'origin/falcondev' into central/falcondev
|
| * 7910f7e (central/merge-requests/18) from forked try
|/
*   e757ac2 Merge remote-tracking branch 'origin/merge-requests/17' into falcondev
|
| * 0f09a9e (central/merge-requests/17) new approach
|/
* 2fa49fb check autonomous commit
*   226c1e9 Merge branch 'falcondev' into 'falcondev'
|
| * f89bb8a (central/merge-requests/16) check online merge
|/
* 2d3def9 (central/merge-requests/15) no fail
* 8daf9c6 (central/merge-requests/14) success pls
* 64afe76 (central/merge-requests/13) Add one more commit
* 8d23993 (central/merge-requests/12) readme md merge
* 2053107 (central/merge-requests/11) check merge commit
* d6a2590 (central/merge-requests/10) check merge commit
* cb85533 (central/merge-requests/9, central/merge-requests/8, sourodeep.c/dummy_testing-falcondev) new change
* 8c27c2d (central/merge-requests/7) changes to merge
* bebf815 (central/merge-requests/6) Merge
* 33d4f61 (central/merge-requests/5, central/merge-requests/4, central/merge-requests/3) Again change
* 0d23cd4 (central/merge-requests/2) Update README.md
| * 3d8c3af (origin/master, origin/HEAD, central/merge-requests/1, master) Update README.md

TL;博士

为了立即修复,我建议使用以下命令序列:

git fetch origin
git checkout master
git merge --ff-only origin/master  # make sure we are up to date
git fetch <fork_repo_url> <fork_branch>
git merge --no-ff FETCH_HEAD -m "<insert a GOOD merge message here>"
git push origin master

好的合并消息由您创建。 Git 提供的标准低质量合并消息是">的合并分支"。 这描述了所执行的操作,但没有给出执行该操作的原因。 这是一条糟糕(低质量)的消息,因为合并本身的存在也会记录所执行的操作,但不会记录特定的 URL 和分支名称。 特定的 URL 和分支名称通常没有用,因此实际上,此合并消息是"此提交是合并提交",这本身是完全冗余的,因此毫无用处。

理想情况下,您应该添加一个远程来命名分叉,之后您可以稍微缩短它:

git fetch other-remote
git merge --no-ff other-remote/<fork-branch> -m "..."

(之后,省略-m参数会插入 Git 的标准低质量合并消息,如果您正在自动化此操作并且无法提供更好的合并消息,这将很方便。

您的直接问题与进行合并提交无关(尽管这里也可能有问题)。 这一系列命令的问题:

git fetch <Fork_Repo_URL> <Fork_Repo_Branch>
git checkout -b <Branch_Name> FETCH_HEAD
git fetch origin
git checkout origin/master
git merge --no-ff <Remote_Name>-<Branch_Name>
git push origin master

位于第二组命令的中间,或者位于最后一个命令的中间,具体取决于您希望如何处理此命令。git merge命令也可能存在错误,但我们稍后会谈到这一点。

命令:

git checkout origin/master

产生 Git 所谓的分离 HEAD。这种状态没有根本性的问题;如果您打算使用它,您只需要了解它的作用。

最后一个命令:

git 推送源头

让你的 Git 在origin调用 Git,并请求他们将他们的名字master设置为你的 Git 当前以你的名义存储的提交哈希 ID,master(在向他们提供实现此目的所需的任何新提交之后)。 如果master及其master都持有相同的提交哈希,则此步骤将一无所获。 您的中间命令进行了新的合并提交,但尚未将其放在任何分支上,因为您正在使用分离的 HEAD 进行操作。

您有多种选择来解决此问题。 例如,您可以将第二组三个 Git 命令替换为:

git fetch origin
git checkout master
git merge --ff-only origin/master  # make sure our master matches theirs
# do whatever is required here if this step fails
git merge --no-ff <Branch_Name>

(请注意,我已将参数更改为此处git merge -no-ff以使用您在第一对命令中创建的名称。 我们稍后会看到更好的方法来解决这个问题。

这样,中间一组命令的结果是创建一个新的合并提交(即使可以快进,--no-ff也会强制执行真正的合并,master更新您的(本地)分支名称以包含新提交。 也就是说,我们刚刚进行的新合并现在on branch master,正如git status会告诉您的那样。 这将馈送到最后一个命令(git push origin master),以便git push转移到您刚刚进行的新合并提交origin,然后请求他们将master设置为指向此新合并提交。

或者,我们可以通过将最终的git push命令更改为:

git push origin HEAD:refs/heads/master

此请求使用更长/更详细的refspec,HEAD:refs/heads/master代替简单的 refspecmaster。 这些 refspec 参数是我们传递给git push的参数,并在远程的名称或 URL 之后git fetch。 也就是说,命令的一般形式是git pushremoterefspec1 refspec2 ... refspecNremote后的每一个参数都是refspec

在第二种最简单的形式中,refspec 只是两个由冒号分隔的标识符:。 标识符HEAD意味着推送的来源——要从我们的(本地)Git 推送到远程 Git 的提交——应该从HEAD读取。 第二个标识符,refs/heads/master,是Git 中master分支的全名:这是我们让 Git 要求他们的 Git 更新的名称。

refs/heads/master,拼写整个事情的原因是,当我们使用更简单的masterrefspec 时——没有冒号的 refspec——Git 由此直觉到我们指的是我们的master分支,因此我们还必须表示它们的master分支。 但是我们预计,在这种情况下,我们没有分支:我们有一个分离的HEAD。 我们的 Git 将无法推断我们因此也必须表示他们的master分支。 也许我们的意思是他们的标签master.1通过阐明refs/heads/master我们消除了所有歧义:我们绝对是指他们的master分支

让我们也来看看你展示的这段特殊的代码:

git fetch <Fork_Repo_URL> <Fork_Repo_Branch>
git checkout -b <Branch_Name> FETCH_HEAD

您创建了一个名为Branch_Name的(本地)分支,但随后从不使用它。 为什么?


1创建一个名为master的标签是一个非常糟糕的主意,如果没有人这样做,我们实际上不需要在这里仔细使用refs/heads/master。 所以我们真的只是小心翼翼地避免在这里被别人的错误绊倒。


Git 是关于提交的,而不是关于分支的

首先,Git 实际上是关于提交的。 Git 与文件无关——尽管文件确实作为快照存储在提交中——并且主要为我们这些笨蛋提供分支名称,他们无法处理大而丑陋的提交哈希 ID。 任何一个提交的真实名称都是其丑陋的大哈希ID,例如08da6496b61341ec45eac36afcc8f94242763468。 没有这个哈希 ID,Git 什么也做不了。 但是人类就是无法处理哈希ID(快速,那是08da64,还是08da46?你必须回头看看它才能找到答案吗?你能记住从一分钟到下一分钟的整个过程吗?)。 然而,人类可以记住像master这样的名字。 所以 Git 会让我们使用名称来替换这些哈希 ID。

每个哈希 ID 都保证是唯一的。 但是,宇宙中的每个 Git 都需要同意两个相同的提交具有相同的哈希 ID,以便您可以通过git clonegit fetchgit push将提交从一个 Git 传递到另一个 Git。 如果你能找到两个具有相同哈希ID的不同提交,你基本上已经找到了一种"破坏"Git的方法。 有关此内容的更多详细信息,请参阅新发现的 SHA-1 冲突如何影响 Git?

正如我们刚才所说,每个提交都会存储一个快照。 也就是说,它具有每个文件的完整副本。阿拉伯数字这是提交的数据,但它也有重要的元数据,例如谁进行了提交,何时以及为什么:他们在提交时提供的日志消息,告诉我们这个新提交的目的是什么。 这里最重要的元数据之一是每个提交都存储其直接前身或提交的原始哈希 ID。 大多数提交只有一个父哈希 ID。 合并提交被定义为存储两个或多个父哈希 ID 的任何提交(其中大多数只存储两个)。

这些父哈希 ID 是 Git 查找先前提交的方式。 由于每个提交都存储其直接父级,因此我们始终可以从最后一次提交开始并向后工作。 如果我们让单个大写字母代表真正的提交哈希 ID,则哈希 ID 为H的提交存储其父级的哈希 ID,我们可以将其称为G。 我们说提交H点来提交G

G <-H

同时,提交G具有父哈希 ID,因此它指向自己的父F

<-F <-G <-H

G一样,F指向它的父级。 如果整个存储库正好有八个提交,它们可能如下所示:

A <-B <-C <-D <-E <-F <-G <-H

提交A是我们做过的第一次提交,所以它不能指向它的父级。 它没有父项,因此它根本不列出任何父项。 这种相当特殊的提交是根提交,每个非空存储库至少有一个。3

Git 的大部分工作只是在必要时遵循所有这些指针。 这些提交及其内部向后箭头是存储库中的历史记录。每个提交都有其所有文件的单独快照。 Git 通过比较任意两个提交来生成差异列表。 提交中的更改因父级而异,级提交由子级中的哈希 ID 记录。 就这么简单。

但请注意,真正的哈希 ID 看起来是随机的,而且又大又丑。 我们可以从上图中找到H,因为H是最后一个字母,但这不适用于真正的哈希 ID。 因此,Git 必须有一种方法将上次提交的哈希 ID 存储在某个分支中,并为我们人类提供一种识别该提交的方法。 这就是分支名称的用武之地。


2也就是说,它有一个完整而完整的文件,其中包含它拥有的每个文件,但这有点多余。 这里的想法是,如果您进行了一些具有README文件的提交,然后进行第二次提交,其中您没有更改README文件,则第二次提交具有README文件的副本。 Git 可以并且确实共享两个相同的文件,它将其存储为blob 对象,这些文件具有哈希 ID,就像提交一样。 因此,无论您提交文件的一个特定版本多少次,存储库都只存储它的一个副本。 由于所有内部 Git 对象都是完全只读的,因此很容易共享它们。 它们无法更改,因此重用某些现有对象只是重用其哈希 ID 的问题。

3大多数存储库可能只有一个根提交,但一旦您知道如何进行,您就可以进行额外的根提交。 您还可以通过将 Git 连接到一些不相关的 Git 来获取新的根提交 - 您从未从中git clone过的 Git,也从未从它的克隆中克隆过,等等。 另一个 Git 有自己的根提交,具有自己唯一的哈希 ID。 您获取他们的提交,以便他们现在被复制到您的仓库中,现在他们的根提交是您自己的 Git 中的另一个根提交。


分支名称是指向提交的指针

分支名称只是将上次提交的哈希 ID 存储在分支中。 要了解其工作原理,请假设我们的八次提交存储库有一个分支名称master名称master将通过存储提交H的原始哈希 ID 来指向提交H,我们可以像这样绘制它:

A--...--G--H   <-- master

如果我们现在进行一个新的提交,Git 会将新提交的父哈希 ID 设置为H,写出新提交以发现其哈希 ID,并看到提交的哈希 ID 是I(好吧,真的,一些大丑陋的哈希 ID):

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

I

现在 Git 只是简单地将I的哈希 ID(无论它到底是什么)写入名称master中,给我们:

...--G--H--I   <-- master

该名称现在指向最后一个提交,就像往常一样。 从最后一次提交开始,我们(或 Git)将按照嵌入在每次提交中的向后箭头向后工作。 (我在这里将它们绘制为线条只是因为它在 StakcOverflow 上更容易,尤其是当我不得不在H下方的对角线上绘制提交I时。 如果你能画出好的向后箭头,那么每当你画提交时,这不是一个坏主意。 这提醒人们 Git 总是向后工作。

HEAD是我们通常附加到分支名称的东西

每次我们进行新提交时,Git 都会将新提交的哈希 ID 写入分支名称。 但是——哪一个?为了使多个分支名称正常工作,在 Git 存储库中,Git 需要记住我们在哪个分支上

Git 这样做的方式是"附加 HEAD"。 特殊名称HEAD,像这样用全大写字母书写,通常附在一个分支名称上。 让我们回到我们的八次提交状态:

...--G--H   <-- master (HEAD)

现在让我们创建一个新名称dev,它也指向提交H。 我们将HEAD附加到master,因为我们将使用git branch dev来创建dev,这样我们就有了:

...--G--H   <-- master (HEAD), dev

现在让我们创建一个新的提交I,然后J另一个提交。 由于HEAD附加到master,这是 Git 将更新的名称

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

现在让我们git checkout dev,它将从提交H而不是提交J重新填充我们的工作区域,并将HEAD附加到名称dev

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

现在我们可以再做两个提交:

I--J   <-- master
/
...--G--H

K--L   <-- dev (HEAD)

这个操作,即使用当前分支名称随着我们的前进而创建新提交,这就是我们构建这些分支的方式。这些名称仅保存每个分支最尖端提交的哈希 ID。提示是 Git 对此的技术术语。

如果我们现在git checkout master选择提交J并命名master,然后运行git merge dev,Git 将构建一个真正的合并提交。 进行此合并的机制涉及查找合并基提交 - 两个分支最后一次"一起看到"的点,在这种情况下,这显然是提交H。 我们不会在这里详细介绍,但结果是这样的:

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

master的名称现在指向合并提交MM像往常一样有一个快照,但不寻常的是,有两个父母JL。 当 Git 从M向后工作时,它会转到两个提交(尽管一次一个)。 从M开始并向后工作的历史包括I-JK-L,以及从那里向后工作,HG等。

请注意,没有其他不同! 分支名称仍然每个指向一个提交。 每个提交仍然有一个快照。 每个提交仍然有一些父项。 合并提交的唯一特别之处在于它有多个父级,因此,历史记录在合并时发散(在这种情况下,在H处重新收敛)。

拆卸头

一旦你理解了上述内容,一个分离的HEAD真的非常简单。 Git 只是将原始哈希 ID 直接存储在名称HEAD中,而不是将HEAD附加到分支名称并从分支名称中获取哈希 ID。 因此,我们可以:

...--E--F   <-- name

G   <-- HEAD

如果我们在此状态下进行另一个新提交,则只有名称HEAD会记住它:

...--E--F   <-- name

G--H   <-- HEAD

如果我们现在git checkout name回到分支上,那么很难找到新提交的哈希 ID:

...--E--F   <-- name (HEAD)

G--H   ???

GH的实际哈希ID是什么? 我不知道,你也不知道。 你能找到他们吗? 如果是这样,也许你可以通过使用它们的原始哈希 ID 或创建一个指向它们的新名称来取回这些提交。 如果没有,也许不是。 例如,如果 commitH的哈希 ID 在窗口中仍然可见,则可以将其剪切并粘贴到git branch命令中:

git branch recover-it <some big ugly hash ID>

这给了我们:

...--E--F   <-- name (HEAD)

G--H   <-- recover-it

分支名称移动,可以根据需要创建和销毁

所以以上总结是:

  • 每个提交都有自己唯一的哈希 ID。
  • 分支的最后一次(或提示)提交是以某个名称存储的提交。
  • 您可以随时创建新名称。 这里唯一的约束是你必须已经有提交 - 你必须有一些现有的提交,你可以找到其哈希ID,或者用其他名称命名。
  • 您可以随时删除名称,但如果这可能会丢失查找某些提交哈希 ID 的唯一方法,请小心! (Git 会尽量小心你,在各种情况下需要--force标志或git branch -D。 这段代码有点棘手,并且随着时间的推移而发展;不同版本的 Git 对删除的容易或困难有不同的规则。
  • 创建提交时,分支名称会自动移动。
  • 特殊名称HEAD通常附加到分支名称,但在分离的 HEAD模式下,不是

不过,还有一件关键的事情需要注意:分支名称是某个特定 Git 存储库的本地名称。

远程跟踪名称

同样,分支名称是某个特定 Git 存储库的本地名称。如果我克隆了您的 Git 存储库,我可能会有一个master和/或dev. 这些是我的master我的dev;他们不是你的master,也不是你的dev。 我可以进行新的提交并更改存储在我的masterdev中的哈希 ID,无论我怎么做。

但是由于我从您的存储库获得了所有初始提交,因此我的所有初始提交哈希 ID都与您的所有 Git 哈希 ID 匹配。 我想记住,你的master指向,比如说,a123456.... 这样我可以看到,自从我克隆您的存储库以来,我已经创建了两个新的master提交。

我的 Git 记住你的Git 分支名称的方式是我的 Git 有远程跟踪名称我的 Git 有一个origin/master要记住你的 Git 在哪里有你的master,我最后一次和你的Git 交谈:

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

在这里,我进行了两个新的提交,您没有。

当然,分支名称会移动。 您的 Git 可能有新的提交。 他们可能在你的master. 所以我可以使用git fetch将我的 Git 连接到您的 Git:

git fetch origin

事实证明,您也进行了两次新提交。 他们有自己的丑陋的大哈希ID,但我就叫他们KL。 我的 Git 获得了这两个提交,并看到您的master指向 L',所以我的 Git 现在有这个:

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

K--L   <-- origin/master

假设我现在运行:

git merge origin/master

这将创建一个新的合并提交M,使用H作为合并基础,JL作为两个分支提示提交:

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

我们以前没见过吗?

扫描到我dev合并到master中的示例。 是的,我们之前已经看到了这个确切的情况。 唯一真正的区别是我使用的是自己的dev;这次我使用的是我的远程跟踪名称origin/master而不是我的分支名称dev

因此,像origin/master这样的远程跟踪名称只是一个保存一个提交哈希 ID 的名称。 从这个意义上说,它就像一个分支名称。 但已经有一个很大的关键区别。如果我运行git checkout origin/master,我会得到一个分离的 HEAD:

I--J   <-- master
/
...--G--H

K--L   <-- HEAD, origin/master

如果我现在运行git merge master我将再次收到提交M,但像这样:

I--J   <-- master
/    
...--G--H      M   <-- HEAD
    /
K--L   <-- origin/master

这个M还有第二个重要的区别,但我只是脚注它。5

关于远程跟踪名称,要记住的最后一件重要事情是,默认情况下,git fetch origin会更新所有origin/*名称。 也就是说,你的 Git 调用了他们的 Git。 他们的 Git 列出了他们所有的分支名称。 您的 Git 通过更新您自己的所有分支名称的内存来结束fetch操作。

您可以限制 Git 更新的远程跟踪名称。git pull命令调用git fetch的方式通常确实会以这种方式限制它们。 我建议根本不使用git pull,但这也是一个意见问题;您可以成功使用git pull,只要您记住这意味着使用特定选项运行git fetch,然后使用其他选项运行第二个 Git 命令。


4Git 调用这些远程跟踪分支名称。 我不喜欢这个短语中多余的单词分支,所以我省略了它,但你应该知道 Git 文档把它放在那里。

5此合并提交作为其第一个父级,已提交L。 提交J是其第二个父级。 父项的顺序有时并不重要——Git 会沿着两条路径返回历史记录——但有时很重要,因为 Git 有一个--first-parent选项,可以命令,例如git log当你进行合并时告诉它,只遵循第一个父级。 这个第一父属性何时以及是否对您有用取决于您如何使用 Git,但值得注意的是,git pull倾向于建立这些类型的交换链接:许多人认为应该是第一个父属性的提交是第二个,反之亦然。 这都是意见问题,但意见足够强烈,让Bitbucket提供了一个可选设备来防止他们所谓的狐步合并。


使用 URLgit fetch,与使用遥控器

在到目前为止的所有示例中,我们都使用了git fetch origin或仅git fetch从 Git 获取新提交,当我们的 Git 在名为origin存储的 URL 上调用某人时,我们的 Git 会获得新的提交。 但您正在使用:

git fetch <url> <branch-name>

这种形式的git fetch要古老得多,可以追溯到 2005 年或更早,在发明像origin这样的远程名称之前。

当使用这种形式的git fetch时,没有远程跟踪的名称。 像origin/master这样的远程跟踪名称是通过获取远程的名称、origin及其分支名称master构建的。 如果没有远程名称(只有 URL),就没有地方构建远程跟踪名称。

在远程名称之前的糟糕过去,我们不得不以这种方式做事。 通过运行:

git fetch <url> <branch-name>

我们将git fetch限制在 URL 处调用 Git 并仅询问该分支。他们的 Git 会说,例如,好吧,我的developa123456...并移交提交a123456...以及我们需要的任何早期提交,以使我们的 Git 能够从a123456...向后移动到根提交。

因此,我们现在将拥有:

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

K--L   [a123456...]

我们必须将提交L的实际哈希 ID 隐藏起来,无论它是什么,都存储在某个地方。某处是特殊名称FETCH_HEAD

请注意,FETCH_HEAD被每个git fetch命令覆盖。 这个名字FETCH_HEAD现在就足以找到提交L,在我们刚刚运行的那个git fetch之后,但它不会持久。 下一个git fetch将覆盖FETCH_HEAD,如果我们不快速将L的哈希 ID 保存在某个地方,我们将丢失它,并且永远无法在 Git 存储库中随机查看的哈希 ID 中找到它。

所以这就是为什么你有第二个命令:

git checkout -b <branch> FETCH_HEAD

名称FETCH_HEAD的功能与远程跟踪名称相同,但仅在下一个git fetch之前。 我们使用远程跟踪名称的糟糕替代品来创建指向同一提交的本地分支名称:

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

K--L   <-- branch

这是我们分支名称,我们现在可以随意使用它。

这会使我们的存储库变得混乱。 为分叉创建一个遥控器是一个更好的主意:

git remote add xyzzy <fork_repo_url>

(当然,选择一个比xyzzy更好的名字)。 现在我们可以运行:

git fetch xyzzy

并获取或更新我们的远程跟踪名称xyzzy/masterxyzzy/developxyzzy/feature/tall等。 我们不必担心处理在每次新获取(包括对origin的获取)时被覆盖的FETCH_HEAD文件,这可能会弄乱我们对 fork-repo 中的哈希 ID 的内存。

git checkout falcondev
git pull central/falcondev
git merge --no-ff origin/falcondev

最新更新