同样的错误存在于数十亿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.html、blah.html 和foo.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 确定)。