如何在不更改远程文件的情况下更新远程 git 回购历史

How to update remote git repo history without altering remote files

我知道这可能不是最佳做法,但我正在尝试使用 git push 作为 Web 项目的部署方法。我们的远程仓库托管在我们自己的服务器上,并且与我们的生产文件夹位于同一文件系统中。

我的目标主要是使用 git push 将我们的项目推送到远程仓库,服务器工具 (plesk) 在推送后自动将仓库复制到我们的生产文件夹。这部分工作正常。

问题是我还想推送我们编译的 CSS、JS 和其他构建工件——git 没有跟踪它们(通过项目 .gitignore 文件) --连同回购协议的跟踪文件。我想让这些文件不被跟踪,我想尝试使用这样的过程(而不是单独的部署工具),因为 git push 是多么快速和简单(如果我不这样做,那将是理想的'不需要在组合中添加其他工具)。

到目前为止,我的尝试使我编写了一个简单的部署 shell 脚本,如下所示:

# ...

if grunt build-full; then
  # temporarily force add and commit ignored build files/dirs to repo
  git add ${build_files[@]} -f &&
  git commit ${build_files[@]} -m "Add compiled css, js, etc for push deploy"

  # push if successful
  if [ $? -eq 0 ]; then
    # update remote (just in case)
    git remote update $remote
    
    # push 
    git push $remote $repo -f
    
    # remove temporarily added files, commit removal
    git rm ${build_files[@]} -f --cached &&
    git commit -m "Remove temporary build files" &&
    
    # reset repo to before removal and add commits to remove unneeded commits
    # (while keeping working directory files), push to sync remote 
    git reset HEAD~2 &&
    git push $remote $repo -f
  fi
fi

想法是临时添加被忽略的文件,推送它们,将它们从存储库中删除,同时将它们保留在文件系统中(通过 git rm --cached),然后通过删除同步远程(通过另一个推送)来自 repo 的临时添加的构建文件,但也将它们保存在远程文件系统中。理想情况下,我想在之后删除“部署”提交(或最多只留下一个)——因此 git reset——但这不太重要。

这似乎在最后一点之前运行良好:最后一次推送只是删除了服务器上的构建文件(即使它们保留在我的本地工作目录中)。如果我尝试省略最后一次推送,服务器的文件系统会反映我正在寻找的内容(构建文件仍然存在),但是远程会领先于本地(因为 git reset-- 如果我落后的话也删除重置)。

有没有办法在不删除远程文件系统上刚刚推送的文件的情况下推送未跟踪的构建文件(通过临时添加或其他方式),然后在本地和远程再次将它们从跟踪中删除?或者有更简单的方法吗?

Is there a way to push untracked build files (by temporarily adding them or otherwise) and then remove them from tracking again on both local and remote without removing the just-pushed files on the remote filesystem?

可能不会。问题不在,或者至少不完全在 Git 中,而是在您使用的任何部署软件中。如果您(ab?)使用 Git 作为部署系统,那么您用来将 Git 转换为部署系统的脚本就是问题所在:Git 是一个 tool-set,并且必须注意在何种情况下使用何种工具。

请注意,在本地,您以一种特定的方式专门使用一种特定的 Git 工具:

git rm ${build_files[@]} -f --cached

实际上,您必须让您的部署系统也执行此操作(甚至可能按字面意思执行)。我不熟悉 Plesk 的内部结构,无法说明是否有办法让它做到这一点。

此答案的其余部分是可选的,但可能对您的目的有用。特别是 一种不同的处理方式,它允许您使用 Git 作为您的部署系统。您只需要停止部署您今天正在部署的提交即可。使您的 to-be-deployed 提交略有不同,否则继续按照您的方式进行。

如何理解正在发生的事情(以及你将要做什么)

Git 实际上就是 提交 。每个提交都包含每个文件的完整快照——或者更准确但显然是同义反复的,它包含的每个文件的快照。这里的想法是快照不是增量:它没有说 使用此提交,从获取上一个提交开始,然后进行这些更改 而是 要使用这个提交,这里是你的文件

这意味着 未跟踪的文件 不在提交中。如果文件 F 未被跟踪并且您进行了新的提交,文件 F 只是 不存在 。这背后的机制是 Git 的 index,它包含将进入 next 提交的每个文件的副本。未跟踪文件是指不在索引中的文件。不在索引中的文件也可以忽略,这意味着 Git 通常不应将其添加 索引,并且不应抱怨其 untracked-ness。

当您 运行(注意,我删除 && 仅供讨论):

git add ${build_files[@]} -f

这会强制添加这些文件,即使它们被标记为“被忽略”。现在 在 Git 的索引中的那些文件的副本,所以现在,如果您进行新的提交,它们将在该提交中。下一行当然是:

git commit ${build_files[@]} -m "Add compiled css, js, etc for push deploy"

这会产生新的提交。新提交包含 所有 Git 索引中的文件。在命令行中提及 ${build_files[@]} 是不必要的,因为命令行中列出的文件的默认 git commit 操作是使用 --include 选项而不是 --only 选项。效果就好像你在提交之前对每个文件再次 运行 git add1 所以你可以把它写成 git add 您已关注:

git commit -m "Add compiled css, js, etc for push deploy"

无论哪种方式,这都会添加一个新的提交,其中包含所有以前的文件加上 forcibly-added 文件。那是因为索引不改变当你提交:2你复制一些文件进去,然后git commit把索引进入提交,但索引本身保持不变,现在与您刚刚进行的提交匹配。 (由于提交是 来自 索引,因此它们必须匹配。)


1git add <em>F</em> && git commit 之间存在技术差异git commit --include <em>F</em> 就使用哪个索引文件和发生什么而言在提交 失败 的异常情况下,但对于成功提交,最终结果足够接近,可以将其视为单独的 add-and-commit。如果使用 --only 选项,如 git commit --only <em>F</em>,情况要复杂得多: 现在涉及三个个索引文件。 --include 选项不会创建任何额外的索引文件。所有提交都会创建一个临时的 index.lock--include 使用 indexindex.lock 的方式与 git commit 既没有 --include 也没有 --only,但在其他方面很容易类比为 add-then-commit;但是 --only 很棘手。

2同样,git commit --only 在这里特别棘手,因为 Git 使用其三个独立的索引文件进行了一系列花哨的技巧,通常是风成功提交后更改主索引。


提交有编号,记录两件事

提交具有数字 ID,但这些 ID 不是连续的:它们是散列。每个提交的哈希 ID 对于该特定提交是唯一的。没有其他提交会拥有相同的哈希 ID——不仅在 this Git 中,而且在 any Git this 中Git 曾与。所以如果你知道一些提交的哈希 ID,你可以问你的 Git 到 etract that commit (provided your Git has that commit course).如果您的 Git 正在与其他 Git 交谈,两个 Git 可以就他们拥有的提交以及他们可能想要的提交达成一致。

虽然提交存储快照,但这并不是它所做的一切。它还存储一些 元数据 ,即关于提交本身的信息,例如提交人、时间和原因——您在上面 -m 中提供的日志消息。此元数据中的一项专门针对 Git 本身,即 上一个 提交的哈希 ID(提交编号)。这就是历史的工作原理:提交的历史只是它之前的提交。

合并提交有点特殊,因为它们有两个(或更多)以前的提交:通常第一个像任何提交一样是它之前的提交,第二个保存的哈希 ID 是合并的提交。否则,合并提交就像任何其他提交一样:它具有所有文件的完整快照。

要查看某些提交中发生的情况,Git 只需将两个 提交提取到临时in-memory 区域。由于内部存储格式,Git 可以缩短很多这项工作:文件自动 de-duplicated 并且很容易判断这两个提交是否只是 re-used 某个文件。从两个文件中提取了所有文件(或者不为简单的 files-are-same 情况烦恼),Git 检查哪些文件相同,哪些不同,然后 执行完全没有关于相同的那些。对于那些不同的,Git 计算一组更改,这些更改将修改较早提交的文件以生成较晚提交的文件。

因为这就是 Git 显示 提交的方式,有些人认为这就是 Git 存储 提交的方式犯罪。但事实并非如此!提交只是一个快照。同时,历史记录——每次提交与其父项或父项之间的联系——让 Git 向您展示软件是如何随着时间的推移而演变的。

要使用提交,我们必须提取它

Git 中的文件以压缩和 de-duplicated 形式存储,并一直冻结——或者至少,只要存储库中的任何提交继续引用它们。这些文件实际上 不能 被大多数 non-Git 软件使用。为了使用它们,我们 Git extract 它们,进入 working treework-tree .

当 Git 提取一个提交时——使用 git checkout 或者,自 Git 2.23,git switch——Git 通过首先填写自己的提交来这样做指数。这意味着索引包含提交的所有文件,准备进入新的提交。 Git 还会将文件复制到您的 work-tree,无论它在哪里(通常就在您工作的地方)。3 只有 in 提交现在在索引中,但是 all 提交的 in 文件现在在索引中.之前存在于您的 work-tree 中的任何未跟踪文件都保留在您的 work-tree.

如果我们从提交 C1 切换到提交 C2,并且提交 C1 有一些提交 C2 的文件 F 具体 省略 ,Git 将知道删除 F 来自你的 work-tree。它知道删除 F 的原因是 F 在 Git 中是 also指数。如果它在索引中,它来自提交 C1,因此从 C1 移动到 C2表示 从索引和 work-tree.

中删除文件

3托管系统通常有 存储库,没有 work-tree。但是,您可以使用 GIT_WORK_TREE(环境变量)或 --work-tree(标志)分配 temporary work-tree,当您这样做时,存储库暂时变成non-bare。即使是裸存储库仍然有一个索引,如果您不覆盖 Git,Git 将在使用此临时 work-tree.[= 时使用 otherwise-unused 索引。 64=]

(即使使用 non-bare 存储库,在现代 Git 中,您也可以在创建时使用 [=40= 将 Git 目录与 work-tree 分开]。我不认为它经常被使用。它刚刚修复了一个相当严重的错误,其中 --separate-git-dir 可以破坏目录内容;这应该在下一个 Git 版本中。)


git rm --cached

当我们使用 git rm --cached 时,我们告诉 Git:从索引中删除此文件,而不是从 work-tree.现在它不在 Git 的索引中,但在 work-tree 中,它(也许再次)未被跟踪。我们可以像您一样再次承诺。或者,我们现在可以 git reset 返回,使用 git reset HEAD~1git reset HEAD~git reset HEAD^(所有意思相同)。我们不想提交但也不想删除的文件不在索引中,所以它们不会已删除。

git push

git push 命令将 提交 发送给其他 Git。由于索引和我们的 work-tree 不是提交,因此它们对我们发送的提交没有影响。这就是我们必须提交这些构建产品的原因:这是让 Git 发送它们的唯一方法。 (推送操作可以高效地工作因为 提交由哈希 ID 唯一编号,所以两个 Git 很容易交换关于他们有哪些提交以及他们有哪些提交的信息需要——这也告诉发送方 Git 哪些 文件 接收方已经拥有,因此发送方不需要发送他们已经拥有的副本。)

向其他 Git 发送他们没有但需要的任何提交,git push 操作以向其他 git push 发出的一系列请求或命令结束。 =380=]: 请将您的名字______(例如填写分支或标签名称)设置为______(例如填写提交哈希ID)。或者:将你的名字______设置为______!git push --force),或者我认为你的名字______有 ID ______;如果是这样,将其设置为 ______;告诉我我是否正确git push --force-with-lease,这是一个命令,但有条件)。

如果他们接受 request-or-command,他们现在有一个特定提交的名称。该提交通过其父哈希 ID 返回到一些较早的提交,这又返回到 still-earlier 提交,依此类推。所以现在接收方 Git 有发送方 Git 给它的提交,并且已经添加了新的提交——例如,通过礼貌的请求——或者可能已经丢弃了一些它过去通过的提交一个 force-push.

使用(或滥用)Git 进行部署

部署脚本往往属于两类之一。有一些简单而幼稚的,实现为 post-receive 或 post-update 钩子:

#! /bin/sh

git --work-tree=... checkout -f

例如,还有更高级的:

#! /bin/sh
while read old new name; do
    ...
done

(作为更好的 post-receive 挂钩)其中 ... 部分计算出更新了哪些分支(如果有),并且仅部署一个 particular分支,并且仅在更新时。

所有真正关键的东西都在于如何部署脚本工作。由于我们看不到你的,我们不确定它是如何工作的——但如果它只是 运行s git checkout -f,它使用接收 Git 的索引和 --work-treeGIT_WORK_TREE 在该命令中设置。因此,该索引跟踪 Git 写入 work-tree 的文件;从提交 X 移动到提交 Y 将删除由于提交 Git 的索引中的所有文件 X 但提交 Y 表明不应该存在。

如果您的部署脚本很花哨,它可以:

  • 构建 build-artifacts,这将是未跟踪的文件。这可能有时间问题,具体取决于构建时间等。

  • 使用两个提交:用git checkout提取一个,然后做其他事情——使用其他Git工具——添加文件来自与提交关联的一些辅助提交,其中包含(仅) otherwise-untracked 文件。这也可能有时间问题,但可能要小得多。

(请注意,即使是检出一次提交的简单方法也可能存在时间问题,因为它可能需要几秒或数十秒才能完成一次大检出。缩小这个 window 尽可能小,智能部署脚本可能会使用 directory-swap 技术,这对上述某些可能性有一定影响,但对下面的提案没有影响。)

虽然它并不花哨,但您可以部署它,而不是提交本身,而是与提交相关联的一些辅助提交。这个辅助提交将有 常规文件 nominally-untracked 文件。

也就是说,您将做与现在完全相同的事情,除了不是部署 masterrelease 或任何分支名称,而是使用一些 other 在 to-be-deployed 文件准备好部署时更新的名称。

to-be-deployed 分支不需要保留任何历史记录。也就是说,每次你去构建它时,你将它构建为一个新的孤立分支,其中包含 to-be-deployed 文件 加上 构建工件文件。这需要您现在使用的 force-push 类型,但意味着您不必“稍后撤消提交:这会自动发生。

这确实意味着您需要部署脚本至少有点聪明:它必须部署您用于此特定情况的特殊分支名称,而不是部署您在正常开发中使用的分支。或者,等效地,您只 将特殊分支推送 到这个存储库,并使用不同的 (non-deployed) 存储库作为您的集中 and/or 备份站点。