首页 > 解决方案 > Git推送旧版本的“--skip-worktree”文件

问题描述

我正在使用 Git 将代码从 DEV 机器传输到 PROD 机器。大多数代码是通用的,但我有特定的配置文件需要在两者之间有所不同。

我希望我的 git 代表 PROD 机器的当前状态。这个想法是从 DEV 推送大多数文件,并且只从 PROD 推送特定的 local_config_file。因此,我在 DEV 上执行了以下命令,以便 DEV local_config_file 不会影响或受 GIT 影响:

git update-index --skip-worktree local_config_file

但是,当我尝试将代码从 DEV 推送到 git 时,出现此错误:

error: failed to push some refs to [my_git]
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again. 

当我运行时git diff origin/branchName branchName (仍在 DEV 上),它只显示与我想要推送的文件的差异(没关系......)以及与旧版本的 local_config_file 的差异,尽管被设置为跳过工作树。

出于实验目的,我尝试将 force 推入 git 并最终在 git 中使用旧版本的 local_config_file ...

任何人都知道如何解决这个问题?谢谢你的帮助

标签: gitgithub

解决方案


我希望我的 git 代表 PROD 机器的当前状态。

好的。

请记住,Git 存储的是提交,而不是文件。提交存储文件——事实上,每个提交都包含每个文件的完整快照——但 Git 一次只处理一个提交。每个 Git 存储库的核心都是提交的集合,每个存储库要么有一些提交,要么没有提交。因此,提交是全有或全无:所有文件,否则您根本没有提交。

这个想法是从 DEV 推送大多数文件......

你不能在 Git 中做到这一点。您必须推送提交

...并且只有来自 PROD 的特定 local_config_file。因此,我在 DEV 上执行了以下命令,以便 DEV local_config_file 不会影响或受 GIT 影响:

git update-index --skip-worktree local_config_file

这不像我认为你认为的那样。这不是下一个问题的直接根源,但在你得到你想要的东西之前,你需要弄清楚它。

error: failed to push some refs to [my_git]
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.

另一方面,这或多或少意味着它所说的。不过,它的建议对于您的特定情况可能略有错误。

关于提交和 Git 索引的知识

使用 Git 时,请记住以下几点:

  • Git 是关于提交的。分支——更具体地说是分支名称——对你有用(对 Git 来说是为了帮助你),但 Git 真的是关于提交

  • 每个提交都有一个唯一的编号。这个数字是一个看起来随机的(但不是随机的)哈希 ID,而不是一个很好的简单计数数字,但它们仍然被编号。这让任何两个 Git 可以相互交谈,并通过比较提交编号就他们是否拥有或缺少某些特定提交达成一致。

  • 每个提交都有两部分:(所有文件的)快照以只读、仅 Git、永久冻结但已删除重复的格式存储——这节省了大量空间,因为大多数文件大多只是重新获得在下一次提交中使用——加上一些元数据,即关于提交本身的信息。元数据包括提交者、提交时间和原因(日志消息)等内容。不过,这也包括前一次提交的提交号。

因为提交中的文件是冻结的、重复数据删除的并且仅限 Git,所以必须提交中提取它们才能使用它们。这些文件被解冻和解压的地方——为你使用而重组或再水化——被称为你的工作树工作树。你选择了一些提交——最终,通过它的内部提交号——然后让 Git 提取那个提交。Git 这样做了,现在您拥有了该提交的所有文件

从这个意义上说,每个文件都有两个副本:当前提交具有​​冻结和去重的仅 Git 副本,而您的工作树具有您可以使用的普通格式副本。

不过,这里是 Git 有点棘手的地方:要进行的提交,Git 不会使用您的工作树副本!那个副本是的,不是 Git 的。相反,Git 偷偷地保留了第三份副本。它不完全是一个副本,因为它是预先去重的,但它就像一个副本。这第三个副本位于 Git 所称的不同地方,即indexstaging area,或者有时——现在很少见—— cache

当您第一次签出提交时,Git 会从该提交中填充其索引,然后也会从该提交中的文件中填充您的工作树。如果您更改某个文件的工作树副本,您通常希望使用git add,它告诉 Git:将工作树副本复制回索引中,替换之前的副本。 这样,索引始终保存您提议的 next commit。(索引中的文件以冻结和重复数据删除的格式保存,随时可以使用,使下一次提交也快速进行。该git add步骤执行压缩和重复数据删除。)

跳过工作树的作用

--skip-worktree位的作用是告诉 Git:如果我的工作树副本与您的索引副本不匹配,那没关系。不要说什么。也不要使用工作树副本来更新索引副本。只需跳过工作树副本即可。 这样,您提议的下一次提交会保留您从某个现有提交中提取的索引副本。

但是,当您使用git checkout切换到其他现有提交时,Git 仍会将冻结格式的文件复制到索引中(有时也会复制到您的工作树中:这些细节因使用 Git 的稀疏检出模式而异)。该位只是意味着当更改工作树副本--skip-worktree时它不会抱怨,并且不会更新索引副本。git add

现在是时候将这部分带回您的特定问题了:

我想 [进行新的提交] 来表示 PROD 机器的当前状态。

Git 将从 Git 索引中的文件而不是工作树中的文件进行新的提交。因此,为此,您必须使用正确的文件加载您自己的 Git 索引。

这个想法是 [使用] 大多数 [ly] 来自 DEV 的文件,并且只有来自 PROD 的特定 local_config_file ...

所以,要做到这一点,你应该确保这个本地配置文件的 Git 索引副本与 PROD 上的索引副本匹配。如果没有:

  1. 将本地配置文件移开;
  2. 清除--skip-worktree标志(git update-index --no-skip-worktree local_config_file);
  3. 将 PROD 配置文件安装为本地配置文件;
  4. 用于git add将正确的文件复制到 Git 的索引中;
  5. 既然索引副本与您希望在下一次提交中拥有的文件匹配,请随意切换配置和/或再次设置标志。

这个过程有几种替代方法。例如,处理这个问题的一个非常标准的方法是确保永远不要在 Git 中存储任何实际的配置文件。仅存储示例默认配置;对于真实的实时配置,在有用的范围内使用示例或默认值,但添加所需的任何自定义,然后根本不让Git 管理该文件。(不幸的是,将配置放入 Git 后,任何从 Git 中删除文件的更新都会导致 Git 更新步骤从工作树中删除文件。所以在这一点上,除非你愿意经历痛苦的​​切换,你在这里有点卡住了。)

如何查看索引文件中的实际内容

当您在--skip-worktree文件的索引副本上设置标志集时,很难知道索引副本的实际内容,因为 Git 不会直接向您显示文件。Git 通常只显示索引副本是否与工作树副本匹配。设置--skip-worktree告诉 Git:不要费心比较(索引和工作树副本)以及抑制更新。

但是,您可以直接查看索引内容:

$ echo 'skip this in worktree' > file
$ git add file
$ git commit -m 'add file to skip'
[master 7cee93e] add file to skip
 1 file changed, 1 insertion(+)
 create mode 100644 file
$ git update-index --skip-worktree file
$ ls
file    README
$ git cat-file -p :file
skip this in worktree
$ echo more >> file
$ git status
On branch master
nothing to commit, working tree clean
$ cat file
skip this in worktree
more
$ git add -f file
$ git cat-file -p :file
skip this in worktree
$ 

上面显示了如何,尽管我对 file 进行了更改filegit status并且git add不理会索引副本。但我可以使用 .查看索引副本git cat-file -p :file。这里的前导冒号:表示让我查看索引中的副本

如果我取消设置--skip-worktree标志,git status开始说更多:

$ git update-index --no-skip-worktree file
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   file

no changes added to commit (use "git add" and/or "git commit -a")

同样,--skip-worktree所做的只是确保它git status不会抱怨并且git add实际上不会添加. 文件的索引副本file仍然存在,并且仍然进入我所做的新提交。

the tip of your current branch is behind its remote counterpart

你的最后一个问题在这里。请记住,如上所述,每个提交都有两个部分:

  • 提交中的数据是所有文件的快照。
  • 提交中的元数据是有关提交的信息,包括作者和提交者(姓名和电子邮件地址)、日期和时间戳等。

在元数据中,每个提交都存储其直接前导提交的唯一编号(或对于合并提交,前导提交,复数)。也就是说,给定一些提交,Git 可以找到该提交之前的提交。Git 将此存储的哈希 ID 称为提交的父级。

Git 无法及时处理此信息。Git 只能倒退。但这已经足够了:给定最新提交的提交编号(哈希 ID),Git 可以从那里向后工作,直到更早的提交。

这些是 Git 中的实际分支。名称——分支名称如和master——develop只是一个技巧:每个名称都存储了最后一次提交的随机哈希 ID,该哈希 ID 被视为该分支的一部分。这意味着重要的是提交本身。这些名称只是让您或 Git 开始使用。

我们可以这样画出这种情况:

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

其中是某个提交链中最后一次提交H的哈希 ID 。为了快速找到 的哈希 ID,Git 有一个名称,例如,存储该哈希 ID:Hmaster

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

使用该哈希 ID,Git 可以提取 commit H,或者,如果您要求它,使用H存储的父哈希 ID(用于的)G返回一次提交以提交G并提取该提交。找到 commitG后,Git 可以使用其存储的父哈希 IDF来提取 commit F。找到该提交后,如果有必要,Git 可以进一步返回。

要在commit 之后进行H的提交,Git:

  1. 收集任何必要的元数据:您的姓名、您的电子邮件地址等;
  2. 获取H的哈希 ID(在名称中很方便master);
  3. 冻结这个,加上所有文件的所有索引副本,到一个新的提交中,我们称之为I;最后,偷偷摸摸但很聪明的举动
  4. I的新哈希 ID 写入名称master。

这意味着我们从:

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

至:

...--F--G--H   <-- master
            \
             I

至:

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

现在我们有一个新的提交master,它只是添加到链中。

要将这个新提交发送到其他Git,我们让我们的 Git 调用他们的 Git,相信他们的Git 也有:

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

提交集及其分支名称中。我们让我们的 Git 向他们发送我们的新提交I,这样他们现在就有了:

...--F--G--H   <-- master
            \
             I

他们的存储库中。然后我们让我们的 Git 询问他们:如果没问题,请设置你的master,以便它指的是I现在提交。

他们说不时,添加:

  ! [rejected]        master -> master (non-fast-forward)

这意味着当我们假设他们有:

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

我们完全错了。他们有这样的东西:

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

在他们的存储库中。我们向他们发送了我们的 commit I,它表示 commitH是它的父级。他们说好的——没关系——现在他们有了:

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

但是后来我们请他们请,如果可以,请移动他们 master以识别 commit I。如果他们这样做,他们将失去他们的承诺J

请记住,Git——<em>任何 Git——的工作方式是从分支名称开始查找最后一次提交,然后向后工作。如果我们让他们将他们设置master为记住 commit I,并且他们从那里向后工作,他们将在 commitI之前进行 commit H,然后像以前一样从那里向后退。但是提交并J没有. H前锋_

如何从这里开始

那么,如果我们不想让他们放弃提交,我们需要做的J就是从他们那里获取他们的提交。我们可以让我们的 Git 调用他们的 Git 并获取commit J,然后将其放入我们的Git 中。这给了我们:

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

我们的存储库中。我们的 commitI仍然存在,并且我们master仍然记得我们的 commit I

我们现在需要的是构建一些新的提交——我们称之为 commit K,尽管实际上它会有一些大而丑陋的随机散列 ID——它建立在他们的 commit 之上J。我们将在我们的 commit 中获取我们的快照,I并将其修改为一个快照,该快照以他们的快照 in 开始,但是在我们从to中J获取我们更改的任何内容。HI

当然,您还有一个额外的并发症。你有一个配置文件,你决定应该将它存储在 Git 中(一个错误,但你被困住了)。可能需要将自己的版本移开——可能完全从自己的 Git 存储库中移出——并放置生产版本,并清除 skip-worktree 位。

一旦你完成了所有这些,你就可以有任何提交——包括HI和/或J——从你的 Git 存储库进入你的工作树和 Git 的索引。 您的配置完全不在您的工作树中;您正在使用生产版本。您现在可以将提交复制到后面I的新提交。有多种方法可以做到这一点,但也许最简单的方法是:KJ

git checkout -b update-prod origin/master    # make a branch to use commit `J`

这给了你一些我们可以像这样画的东西:

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

现在很容易使用git cherry-pick复制 commit I。通常我会调用 copy I',但我一直使用 name K,所以让我们编写命令并绘制结果:

git cherry-pick master

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

您现在可以将旧名称和当前名称重命名master为:old-masterupdate-prodmaster

git branch -m master old-master
git branch -m update-prod master

这给了你:

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

(您可能甚至不再需要该名称 old-master,但将其保留一段时间也无妨。)现在您可以再次在配置文件中设置 skip-worktree 位,将开发配置复制回原处,并尽可能多地对其进行测试。

当您的测试良好时,您可以运行:

git push origin master

这一次,您的 Git 调用他们的 Git,提供您的新提交K,然后要求他们将其设置master为指向现在共享的提交K。他们的 Git 不再有理由拒绝该请求。那么,在他们的 Git 上,这将产生:

...--F--G--H--J--K   <-- master

提交I在他们的 Git 中不可见。它可能在那里,因为您之前发送过它,但是没有人能找到它,因为我们(包括 Git)查找提交的方式是从分支名称开始并使用这些名称来查找最后的提交,然后向后工作,并且在他们的Git中没有名称可以用来查找 commit 。I

一旦你删除了你自己的 name old-master,你也不再有一个方便的方法来查找 commit I,它就好像它从未存在过一样。我们没有理由绘制带有扭结的提交,因为您自己的 Git 现在将拥有:

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

一旦他们接受你的请求,你的 Git 就会更新你自己的——你的origin/masterGit 对他们 Git 的记忆。mastergit push


推荐阅读