Git 接收后部署在随机点停止工作



我有一个 git 的接收后钩子设置,它根据分支签出到开发/暂存/生产。出于某种原因,开发和暂存工作没有问题。但生产不断中断。推送主分支后,更新无法签出到正确的位置,尽管在最初设置后工作。

#!/bin/bash
while read oldrev newrev refname
do
branch=$(git rev-parse --symbolic --abbrev-ref $refname)
if [ "master" == "$branch" ]; then
GIT_WORK_TREE=/var/www/production git checkout -f $branch
elif [ "staging" == "$branch" ]; then
GIT_WORK_TREE=/var/www/staging git checkout -f $branch
else
GIT_WORK_TREE=/var/www/dev git checkout -f $branch
fi
done

我尝试将主分支更改为称为生产的分支,并遇到了同样的问题。最初工作,一段时间后停止,原因我无法解决。

if 语句之所以有效,是因为在 checkout 语句下方添加触摸命令时,会在正确的目录中成功创建文件。这也排除了权限,因为所有 3 个目录在这方面都是相同的。

如果有人有任何想法,或者可以看到可能导致这种行为的东西,那就太好了!

同样的错误存在于数十亿和数十亿1许多部署脚本中。

问题是 Git 有一个索引。

更准确地说,Git 需要每个工作树都有一个索引。阿拉伯数字

裸存储库没有工作树,但 Git 仍然有一个索引 — 如一 (1) 个索引,可在该文件中找到index在裸存储库中找到。 这意味着您可以使用GIT_WORK_TREE或等效项强制存在一 (1) 个工作树,并使用该索引将一个分支签入该工作树。

与许多其他部署脚本一样,您的部署脚本使用一个索引将三个不同的分支签出到三个不同的工作树。 当 Git 相信索引并使用它来构造对假设为一个工作的树进行最小更改时,事情就会出错,您正在检查每个分支。 您将生产分支写入工作树/var/www/production;然后你更新工作树,使用保存在(单个)索引中的状态,它正确描述了(单个)工作树中的内容,以从staging分支更新/var/www/staging中的不同工作树,因此 Git 仅更改必要的文件,使用其保存的知识并相信这就是/var/www/staging中的内容......好吧,你明白了。:-)

治疗方法是做以下各种事情之一:

将三个不同的
  • 工作树与三个不同的索引文件一起使用。 然后索引文件实际上将与工作树匹配,Git 的"进行最小更改"将起作用。 新的内置git worktree add应该是执行此操作的好方法,尽管我还没有尝试过。 从逻辑上讲,设置receive.denyCurrentBranchupdateInstead模式应该会更新相应的工作树。 这需要一个现代的 Git;git worktree进入了 2.5,在 2.6 中进行了一些重要的修复,此后修复了更多,尽管规模较小。注释添加了 2016 年 12 月,但即使在 Git 版本 2.11 中它实际上也不起作用。 它最终可能成为一种选择。

  • 或者,您可以在设置变量的同时设置变量GIT_INDEX_FILEGIT_WORK_TREE,并且只有三个单独的索引文件。 Git 将根据需要创建它们,因此这是您可以对现有部署脚本进行的最小更改:

    GIT_WORK_TREE=/var/www/production GIT_INDEX_FILE=$GIT_DIR/index.production 
    git checkout $branch
    
  • 或者,确保 Git 重建索引和/或工作树。 如果删除整个工作树(或将 Git 指向空的工作树),Git 会注意到当前索引毫无价值。 然后它会重新检查所有内容。

最后一种方法比前两种方法耗时得多,但如果您仔细操作,确实具有优势。 考虑一下 Git 更新文件时 Web 服务器会发生什么情况。 Git 查看索引以查看现在签出的内容,并查看您提供给git checkout的内容以查看签出的内容。 假设必须更新文件index.htmlblah.htmlfoo.css。 Git 更改了其中之一,就在那时,您的 Web 服务器获得了新的连接......并在阅读blah.html的同时阅读index.html.

会发生什么? 谁知道呢? 这里的重点是您的 Web 服务器看到不一致的快照。 它可能不是不一致,而且时间不长,也许这不是问题,但如果你想要真正可靠的软件,你可能想避免它。 从本质上讲,您需要让 Web 服务器读取旧快照,直到新快照完全准备就绪,您可以通过冻结 Web 服务器或将转换作为原子操作来完成。

现在考虑一下如果服务器执行以下操作会发生什么:

newtree=/var/www/newtree.$$
oldtree=/var/www/production.$$
# neither of these trees should exist, but do this
# in case we had a crash or something that left them behind
rm -rf $newtree $oldtree
mkdir $newtree
# populate the new tree
GIT_WORK_TREE=$tmptree git checkout $branch
# freeze / terminate the server (may not need this
# depending on how clever the server is -- it needs
# to notice the changeover)
service httpd stop
# swap the new tree in and the old one out, quickly
# (this is just two easy rename operations)
mv /var/www/production $oldtree
mv $newtree /var/www/production
# unfreeze/resume the server
service httpd start
# finally, delete the old tree (this does not need to be fast)
rm -rf $oldtree

这为您提供了相对最短的服务器停止或冻结的时间(而不是完全终止/停止它,您可能只需向它发送其目录已更改的通知,然后等待几秒钟让它切换)。 代价是您必须暂时同时拥有旧树和新树,并且设置新树比仅交换几个文件需要更长的时间。

偶然

这:

branch=$(git rev-parse --symbolic --abbrev-ref $refname)

有点误导,因为$refname根本不是分支。 它可以是refs/heads/master(master是分支)或refs/tags/v1.2(不是分支 - 它是一个标记)或refs/notes/commits(既不是分支也不是标记)。 这里已经足够好了,但这样做可能更明智:

case $refname in
refs/heads/production) deploy production;;
refs/heads/staging) deploy staging;;
refs/heads/dev) deploy dev;;
*) ;; # do nothing
esac

其中deploy是一个 shell 函数,它将命名分支 ($1) 部署到/var/www/$1。 否则,您将重新部署dev以推送到master和创建标记。


1RIP CES,尽管实际上他从未说过。

2每个工作树还有一个 HEAD,git worktree在这里也可以正确管理,尽管我从未在部署脚本中实际尝试过。 我不是 100% 确定如果部署的分支指向相同的提交 ID 会发生什么:我使用的工作流通常规定无论如何都不会发生这种情况,因此git checkout <branch>总是HEAD移动。 移动HEAD保证git checkout会做一些工作。 使用两个指向同一提交 ID 的分支测试单独的索引、共享 HEAD 方法可能会很有趣,看看会发生什么。

无论如何,对单个 HEAD 大惊小怪的一个副作用是新克隆将签出不同的默认分支(因为默认分支由源的 HEAD 确定)。

最新更新