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

Git post-receive deployment stops working at random points

我为 git 设置了 post-receive 挂钩,它根据分支检出到 dev/staging/production。出于某种原因,开发和登台工作没有问题。但是生产不断中断。推送 master 分支后,更新无法签出到正确的位置,尽管在最初设置后工作。

#!/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

我试过将 master 分支更改为名为 production 的分支,但遇到了同样的问题。最初工作并在一段时间后由于我无法解决的原因而停止。

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

如果有人有任何想法,或者能看到可能导致此行为的原因,那就太好了!

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

问题是 Git 有一个索引。

更准确地说,Git 每个工作树需要一个索引。2

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

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

治疗就是做这些不同的事情:

  • 使用三个不同的工作树和三个不同的索引文件。然后索引文件实际上将与工作树匹配,并且 Git 的 "make a minimal change" 将得到解决。新的内置 git worktree add 应该 是一个很好的方法,虽然我还没有试验过这个。从逻辑上讲,设置 receive.denyCurrentBranchupdateInstead 模式应该更新适当的工作树。这需要一个现代的 Git; git worktree 进入 2.5,在 2.6 中进行了一些重要修复,此后进行了更多(尽管较小)修复。 注意于 2016 年 12 月添加 但它没有' 即使在 Git 版本 2.11 中也能正常工作。它最终可能会成为一个选项。

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

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

最后一种方法比前两种方法耗时要多得多,但如果你仔细操作,它确实有一个优势。考虑在 Git 更新文件时您的 Web 服务器发生了什么。 Git 查看索引以查看现在签出的内容,并查看您提供给 git checkout 的内容以查看 应该 签出的内容。假设必须更新文件 index.htmlblah.htmlfoo.css。 Git 更改其中之一,就在那时,您的网络服务器获得了一个新连接...并在读取 [=87= 时读取 old index.html ]new blah.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

这为您提供了相对最短的服务器停止或冻结时间(而不是 killing/stopping 它完全,您可以只向它发送一个通知,告知其目录已更改,然后等待几秒钟让它切换)。代价是你必须临时拥有新旧树,而且建立新树比只换出几个文件要花更长的时间。

顺便说一下

这个:

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 函数,它将命名分支 (</code>) 部署到 <code>/var/www/。否则,您将重新部署 dev 以推送到 master 和创建标签。


1CES安息吧,虽然他从来没有说过

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

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