Git 重置不会更新远程服务器上的更改

Git reset does not update the changes on the remote server

我目前正在学习 GitHub Actions。我的目标是为小型私人 Web 项目(网站)构建部署管道。除此之外,我注意到一些我不明白的东西。

最好我用一个例子来解释。但首先我的设置:

到目前为止一切正常。

现在我测试了如果我最后一次推送包含错误并且我想撤消它会发生什么。以便页面继续 运行,我可以进行 BugFix。 所以我输入了本地:git reset --hard hash_from_prev_commit。 在本地,提交已重置。使用 git push -f 远程存储库也已更新。但是在远程服务器上它没有被重置。 GitHub 动作输出:

out: /www/htdocs/w019db06
out: On branch master
out: Your branch is up-to-date with 'origin/master'.
out: nothing to commit, working directory clean
err: From github.com:Project-X/git-workflow-test
err: + ffd263c...e5a9ec8 master -> origin/master (forced update)
out: Already up-to-date.
==============================================
✅ Successfully executed commands to all host.

为什么会这样,我必须做什么才能使更改在远程服务器上生效?

TL;DR

您需要将第三个存储库获取到 运行 git fetch,然后是 git reset --hard,如:

git fetch origin
git reset --hard origin/master

如果您愿意在此处对名称进行硬编码 master

让我在这里总结一下,有 三个 个存储库。让我们给他们起个名字:阿尔弗雷德、芭芭拉和猫女。存储库 A (Alfred) 在您的笔记本电脑上。存储库 B (Barbara) 在 GitHub 上。存储库 C 在您的服务器上,无论它是什么。

为简单起见, A 上采取的操作,由一些 Git 进程 运行 因为你 运行 一些 git <em>笔记本电脑上的任何</em>命令,都是“Git A”; B 上的那些(在 GitHub 上)是“Git B”,那些出现在 C 上的是“Git C”。这应该有助于清楚了解每台机器上、每个存储库中发生的事情。

这一行,再加上我不会引用的下一行:

+ ffd263c...e5a9ec8 master -> origin/master (forced update)

是由GitC打印的,因为GitC是运行宁git pull。但是git pull表示:运行git fetch,然后运行第二个Git命令.

背景(有点长,喜欢的可以略读)

现在,在我们继续之前,我们需要再做一些说明:

  1. Git 就是关于 提交 。这与 b运行ch 名称或文件无关:b运行ch 名称有助于一些 Git 存储库 find 提交,并且提交 包含个文件,但我们真正关心的是提交.

  2. 提交是 共享的: 如果存储库 A、B 和 C 相互交叉连接,它们最终都会以 相同 提交。

  3. 提交已编号,带有丑陋的大哈希 ID。通常我喜欢用单个大写字母来代表这些,只要我开始深入字母表,我就可以在这里这样做(A、B 和 C 是各种机器上的存储库,所以我想避免那些字母)。

  4. 每个提交 包含 ,作为其元数据的一部分,一些 previous 提交或提交的原始哈希 ID .大多数提交只有一个先前的哈希 ID,它将提交形成向后看的链:

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

    其中 H 代表链中 最新 提交的哈希 ID。提交H最新的,是链的结束:在这一点之后没有更多的提交。

  5. 如第 1 项所述,Git 存储库将使用 b运行ch 名称 来帮助它 find 提交。但是 b运行ch 名称 不共享: 每个 Git 存储库都有 自己的 b运行通道名称。

现在,有点懒,我倾向于这样绘制提交:

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

这里我们看到当前的 b运行ch 名称——在本例中为 master——如何指向链中的 last 提交。 new 提交的过程在 Git 中完成,通过检查一些提交 (H),使用一些 b运行ch 名称(master),这使得 b运行ch 成为 current b运行ch 并且那个特定的提交成为 current commit。然后我们对工作树中的文件进行操作,使用 git add 来“暂存”它们,并且 运行 git commit 和 Git 构建一个 new 提交 I,指向当前提交:

...--F--G--H   <-- master (HEAD)
            \
             I

作为git commit的最后一步,Git将I的实际哈希ID——一些大而难看的十六进制数——写入名称master

...--F--G--H
            \
             I   <-- master (HEAD)

现在提交链结束的不是 H,而是 I

当我们使用 git pushgit fetch 时,我们的 Git 调用其他一些 Git 并向他们发送我们的新提交 (git push) 或从他们那里获得任何新的提交(git fetch)。这就是共享提交的方式。但在那之后,事情变得有点奇怪:如果我们使用 git fetch,我们的 Git 更新,而不是 b运行ch 名称,而是 remote-tracking 名称。例如,如果我们从 他们的 Git 得到一个新的提交 J,我们现在将有:

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

我们现在可以 add 提交 Jour b运行ch(我们的 master,它独立于他们的 master) 使用各种Git操作,得到:

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

由于各种原因,git push 不同:在我们将新提交发送到其他存储库后,我们(礼貌地)询问他们是否愿意,如果可以,请设置 他们的 b运行ch 某些特定提交的名称。例如,如果他们只提交了 H,我们可以向他们发送提交 I,或提交 I-J,那 添加 到他们的H 像这样,然后让他们设置 master.

问题

让我们回顾一下这一切的起因。你,在你的笔记本电脑上——在 Git A 上——将要 运行:

git reset --hard HEAD~

或同等学历。这会将 your b运行ch name master 移回一个提交。但我们不会一下子就到那里。

假设您开始于:

...--G--H   <-- master (HEAD), origin/master

然后您在 您的 存储库 A:

中添加了提交 I
...--G--H   <-- origin/master
         \
          I   <-- master (HEAD)

你然后运行git push origin master。将新提交 I 发送到 repo B,然后要求他们将 master 指向 I,他们这样做了:

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

然后你有 repo B tickle repo C,其中 运行 git pull origin master 或者只是 git pull (两者做同样的事情)。 Repo C 调用了 repo B 并得到了 repo C 的新内容:commit I:

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

second 命令,您在 C 上的 git pull 运行 是 git merge。这发现不需要真正的合并,所以它只是 添加提交 I 到它的 master:

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

所以现在,回购 A 和 C 匹配(直到都具有指向提交 Iorigin/master 名称,从回购 B 复制并重命名)。 Repo B 的 master 指向提交 I.

现在我们在 repo A 上重置提交 I(哇,终于。)让我们画一下:

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

回购 A 仍然记得回购 B 正在使用提交 I 作为回购 B 的 master。没错!

你现在可能会从 A 运行 git push origin master 得到一个关于它不是快进的错误:

! rejected ... (non-fast-forward)

那是因为你的 Git 与他们的 Git 交谈过,发现你没有要发送的提交,最后礼貌地请求他们将 master 设置为指向 H。这是一个礼貌的请求,他们 忘记 提交 I,他们说

所以你求助于 git push -f 或等价物。这将 git push 的最后一步从礼貌的请求 please set your master to point to H 更改为强制命令:将你的 master 设置为指向 H! 如果他们服从,这会发生在 repo B:

...--G--H   <-- master (HEAD)
         \
          I   ???

请注意,回购 B 不再有提交 I 的名称。您仍然可以通过原始哈希 ID 访问它(GitHub 允许您使用他们的 API 和 Web 界面执行此操作),但您必须知道哈希 ID。

现在,这个 git push 到 GitHub 触发 Repo A 做一个 git pull,这再次只是 git fetch 紧接着第二个 Git 命令,在本例中为 git merge。 Repo C 目前有这个:

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

他们 运行 git fetch origin 并看到,在回购 B 中(在 GitHub 上),master 选择提交 H。这意味着他们可以让他们的 origin/master 指向提交 H,但只能通过执行 forced 更新:这是一个“非快进”,在Git行话。

所以,他们这样做了。现在他们有了这个:

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

也就是说,Repo C 仍然有提交 I。它就在他们 master.

的末尾

他们的 Git 现在 运行 是 git pull 第二 步,即 运行 git merge origin/master(或多或少——此时它实际上在内部处理原始哈希 ID)。这告诉他们将提交 H 添加到以提交 I 结束的链中。提交 H 已经在此链中 ,所以他们打印:

Already up-to-date.

然后什么都不做。

现在问题很明显了(好吧,事情变得一目了然 Git)

这里的问题是 git merge——git pull 的默认第二个命令——只是 错误的 。您不想 合并 新提交,在回购 C 上。您想 切换到回购 B 上某些 b运行ch 的最新提交。在回购 B 的 master.

的情况下,这记录在 origin/master

git pull 命令可以被告知 运行 另一个不同的第二个命令:您可以使用 git rebase 而不是 git merge。但这同样是错误的,甚至可能是“错误的”(如果那是一件事)。你一个都不想要。

Git 不是部署工具

这里的最终问题是您将 Git 视为部署工具,而它根本不是一个。它可以用作,就像螺丝刀可以用作凿子,或者钻床可以用来拧紧螺丝一样。在某种程度上,这只是对工具的滥用。你需要确定你确切地知道你在做什么。