【问题标题】:Git post-receive deployment stops working at random pointsGit post-receive 部署在随机点停止工作
【发布时间】:2016-11-23 08:51:14
【问题描述】:

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

#!/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 个目录在这方面都是相同的。

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

【问题讨论】:

    标签: git bash git-post-receive


    【解决方案1】:

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

    问题是 Git 有一个索引。

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

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

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

    解决方法是做以下不同的事情之一:

    • 使用三个不同的工作树和三个不同的索引文件。然后索引文件实际上将匹配工作树,并且 Git 的“进行最小的更改”将起作用。新的内置git worktree add 应该是一个很好的方法来做到这一点,虽然我没有尝试过这个。从逻辑上讲,将updateInstead 模式设置为receive.denyCurrentBranch 应该会更新相应的工作树。这需要现代风格的 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 重建索引和/或工作树。如果您删除整个工作树(或将 Git 指向一个空的工作树),Git 会注意到当前索引毫无价值。然后它会重新检查所有内容。

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

    会发生什么?谁知道?这里的重点是您的网络服务器看到了一个不一致的快照。这可能不是非常不一致,而且不会持续很长时间,也许这不是问题,但如果你想要真正可靠的软件,你可能想要避免它。本质上,您需要让 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% 确定如果部署的分支指向 same 提交 ID 会发生什么:我使用的工作流程通常表明无论如何都不会发生,所以 git checkout <branch> 是总是在移动HEAD。移动HEAD 保证git checkout 会做一些工作。用两个指向同一个提交 ID 的分支来测试单独索引、共享 HEAD 方法可能会很有趣,看看会发生什么。

    在任何情况下,对单个 HEAD 大惊小怪的一个副作用是新克隆将检查不同的默认分支(因为默认分支由源头的 HEAD 确定)。

    【讨论】:

    • 感谢您的回复。我确实有一个问题 - 如果我要直接对生产分支进行更改并将其推送到服务器,那么 git 不会注意到与它的索引相比的更改并将其推送到 /var/www/production 吗?忽略开发/登台,我在生产中所做的任何更改都不会被检查出来。我理解您指出的问题,但这可能是一个额外的问题吗?或者这是我的部署脚本所期望的吗?谢谢。
    • Git 是否以及何时注意到它的索引与任何给定的部署目录不同步很难预测。它依赖于文件,依赖于 Git 版本,依赖于操作系统,并且在某些操作系统上,还依赖于文件系统。基本上 Git 相信它的索引,直到它有某种证明它是错误的,然后它重建(部分或全部)它的索引。因此,您的索引应该始终与您的工作树保持一致,否则您会看到奇怪的行为。参见stackoverflow.com/questions/22053757/…
    • 还要注意,添加-f 不会改变git checkout 比较索引和工作树的方式,它只是意味着如果git checkout 会覆盖未暂存的更改,则可以随意这样做。 Checkout 并没有真正将当前 branch 与目标进行比较,而是将当前 index 与目标提交进行比较,因此如果production 已被@987654363 更新@ 和你 git checkout production,Git 正在从提交 aaaa... 更改为 bbbb...,并且必须按照索引所述更新文件。
    • 感谢您抽出宝贵时间。没有机会实现这一点,但一旦我这样做,我会接受答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-06-28
    • 1970-01-01
    • 1970-01-01
    • 2014-05-17
    • 1970-01-01
    • 2015-07-16
    • 2014-04-11
    相关资源
    最近更新 更多