首页 > 解决方案 > 如何在不更改远程文件的情况下更新远程 git repo 历史记录

问题描述

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

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

问题是我还想将我们编译的 CSS、JS 和其他构建工件——它们不被 git 跟踪(通过项目.gitignore文件)——与 repo 的跟踪文件一起推送。我想保持这些文件不被跟踪,并且我想尝试使用这样的过程(而不是单独的部署工具),因为它是多么快速和简单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),然后通过从存储库中删除临时添加的构建文件来同步远程(通过另一个推送),但也将它们保存在远程文件系统中。理想情况下,我想在之后删除“部署”提交(或最多只留下一个)——因此——git reset但这并不重要。

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

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

标签: gitshelldeploymentsh

解决方案


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

可能不是。问题不在于,或者至少不完全在于 Git,而在于您使用的任何部署软件。如果您(ab?)使用 Git 作为部署系统,那么您用来将 Git 转变为部署系统的脚本就是这里的问题:Git 是一个工具集,必须注意在哪种情况下使用哪种工具.

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

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

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

此答案的其余部分是可选的,但可能对您的目的有用。特别是,有一种不同的方法来处理这个问题,这将允许您使用 Git 作为您的部署系统。您只需停止部署您今天正在部署的提交。使您的待部署提交略有不同,否则请继续按照您的方式进行。

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

Git 真的是关于提交。每个提交都包含每个文件的完整快照——或者更准确但显然是重言式,它包含的每个文件的快照。这里的想法是快照不是增量:它没有说要使用此提交,首先要获取上一个提交,然后进行这些更改,而是使用此提交,这里是您的文件

这意味着未跟踪的文件不在提交中。如果文件F未被跟踪并且您进行了新的提交,则文件F根本不存在。这背后的机制是 Git 的index,它保存将进入下一次提交的每个文件的副本。未跟踪的文件是不在索引中的文件。不在索引中的文件也可以被忽略,这意味着 Git 通常不应该将它添加索引中,也不应该抱怨它的未跟踪性。

当您运行时(请注意,我删除&&s 仅用于讨论目的):

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 add在提交之前对每个文件再次运行一样。1 因此,您可以将其编写为git add您所遵循的:

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

无论哪种方式,这都会添加一个新的提交,其中包含所有以前的文件以及强制添加的文件。那是因为提交时索引不会改变2你将一些文件复制到其中,然后git commit将索引变为提交,但索引本身保持不变,现在匹配你刚刚所做的提交。(由于提交是根据索引进行的,因此它们必须匹配。)


1在使用哪些索引文件以及在提交失败的异常情况下会发生什么方面存在技术差异,但对于成功的提交,最终结果足够接近,可以将其视为单独的添加-并提交。如果您使用选项,如 中,情况要复杂得多:现在涉及三个索引文件。该选项不会创建任何额外的索引文件。所有提交都会创建一个临时的; 使用and的方式与使用没有nor有所不同,但很容易类比为 add-then-commit;但很棘手。git add F && git commitgit commit --include F--onlygit commit --only F--includeindex.lock--includeindexindex.lockgit commit--include--only--only

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


提交被编号,并记录两件事

提交具有数字 ID,但这些 ID 不是连续的:它们是哈希。每个提交的哈希 ID 对于那个特定的提交是唯一的。没有其他提交将具有相同的哈希 ID ——不仅在这个Git 中,而且在这个 Git 与之对话的任何Git 中。所以如果你知道某个提交的哈希 ID,你可以让你的 Git 提取那个提交(当然前提是你的 Git那个提交)。如果您的 Git 正在与其他 Git 通信,则两个 Git 可以就他们拥有哪些提交以及他们可能想要哪些提交达成一致。

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

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

要查看某个提交中发生了什么,Git 只需将两个提交提取到临时内存区域。由于内部存储格式,Git 可以缩短很多这项工作:文件会自动删除重复数据,并且很容易判断这两个提交是否只是重新使用了某个文件。从两者中提取所有文件(或者不为简单的文件相同的情况而烦恼),Git 检查哪些文件是相同的,哪些是不同的,然后对那些是相同的。对于那些不同的,Git 计算一组更改,这些更改将修改先前提交的文件以生成稍后提交的文件。

由于这就是 Git显示提交的方式,因此有些人认为这就是 Git存储提交的方式。但事实并非如此!提交只是一个快照。同时,历史——从每个提交到其父级或父级的链接——让 Git 向您展示软件如何随着时间的推移而演变。

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

Git 中的文件以压缩和去重的形式存储,永远冻结——或者至少,只要存储库中的任何提交继续引用它们。大多数非 Git 软件根本无法使用这些文件。为了使用它们,我们让 Git将它们提取工作树工作树中。

当 Git 提取提交时——使用git checkout或,从 Git 2.23 开始, ——Gitgit switch通过首先填写自己的索引来完成。这意味着索引包含来自该提交的所有文件,准备好进入新的提交。Git 还将文件复制到您的工作树中,无论它在哪里(通常就在您工作的地方)。3 只有该提交中的文件现在索引中,但该提交中的所有文件现在都索引中。之前存在于您的工作树中的任何未跟踪文件都保留在您的工作树中。

如果我们从提交C1切换到提交C2,并且提交C1有一些文件F提交C2特别省略,Git 会知道从你的工作树中删除F。它知道删除F的原因是F也在Git 的索引中。如果它在索引中,它来自提交C1,因此从C1移动到C2意味着从索引和工作树中删除文件


3托管系统通常具有没有工作树的裸存储库。但是,您可以使用(环境变量)或(标志)分配一个临时工作树,并且当您这样做时,存储库会暂时变得非裸露。即使对于一个裸存储库,仍然有一个索引,如果你不覆盖 Git,Git 将在使用这个临时工作树时使用其他未使用的索引。GIT_WORK_TREE--work-tree

(即使使用非裸存储库,您也可以在现代 Git 中,在创建时将 Git 目录与工作树分开--separate-git-dir。我认为这并不经常使用。它只是修复了一个错误可以破坏目录内容的严重错误--separate-git-dir;这应该在下一个 Git 版本中。)


git rm --cached

当我们使用 时git rm --cached,我们是在告诉 Git:从索引中删除这个文件,而不是从工作树中删除。 现在它不在 Git 的索引中,而是在工作树中,它(可能再次)未被跟踪。我们可以再次承诺,就像你一样。或者,我们现在可以git reset返回,使用git reset HEAD~1or git reset HEAD~or git reset HEAD^(都是同一个意思)。我们不想提交但也不想删除的文件不在索引中,因此它们不会被删除。

git push

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

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

如果他们接受请求或命令,他们现在就有了一个特定提交的名称。该提交通过其父哈希 ID 导致返回到一些较早的提交,这导致返回到更早的提交,依此类推。所以现在接收 Git 拥有发送 Git 给它的提交,并且要么添加了新的提交——例如,通过礼貌的请求——要么可能已经通过强制推送丢弃了一些以前的提交。

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

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

#! /bin/sh

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

例如,还有更高级的:

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

(作为一个更好的 post-receive 钩子)该...部分确定哪些分支(如果有)已更新,并且仅部署一个特定的分支,并且仅在更新时部署。

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

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

  • 构建构建工件,然后是未跟踪的文件。这可能存在时间问题,具体取决于构建所需的时间等等。

  • 使用两个提交:git checkout使用其他 Git 工具提取一个,然后执行其他操作,以从与该提交关联的某个辅助提交添加文件,该提交包含(仅)未跟踪的文件。这也可能有时间问题,尽管可能要小得多。

(请注意,即使是检查一个提交的简单方法也可能存在时间问题,因为它可能需要几秒钟或几十秒才能完成一次大检查。要将此窗口缩小到尽可能小,智能部署脚本可能使用目录交换技术,这对上述一些可能性有一些影响,但对下面的提议没有影响。)

但是,即使它不是花哨的,您也可以部署它,而不是提交本身,而是与提交相关联的一些辅助提交。该辅助提交将具有常规文件名义上未跟踪的文件。

也就是说,您将执行与现在完全相同的操作,只是您将使用其他master名称,而不是部署或其他任何分支名称,这些名称会在待部署文件准备好部署时更新。release

待部署的分支不需要保留任何历史记录。也就是说,每次构建它时,都将它构建为一个新的孤立分支,其中包含要部署的文件构建工件文件。这需要您现在使用的那种强制推送,但这意味着您不必“稍后撤消提交:这只是自动发生的。

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


推荐阅读