在 git hook 中操作 repo 时 -C 和 --git-dir 的区别

Difference between -C and --git-dir when manipulating repo in git hook

我正在编写一个 git post-receive 挂钩,它将克隆一个单独的 repo 作为部署的一部分。它将 repo 克隆到某个文件夹,并在随后的 git 命令中使用 -C 选项将目录设置为检出 repo 的目录(如 man page 中所述)。

当从命令行手动 运行 时挂钩按预期工作,但是当 git 挂钩 运行 时(即收到推送时)命令失败fatal: Not a git repository: '.'。当我将 -C 换成 --git-dir 时,它起作用了。

复制起来相当简单,创建一个裸仓库 git init --bare 并制作一个包含以下内容的可执行挂钩:

#!/bin/bash
set -xe

SOME_REPO_URL=???? # Some repo that is not this one
repopath=/tmp/somerepo

git clone $SOME_REPO_URL $repopath

# 1: This fails when run through the git hook
git -C $repopath checkout -b somebranch HEAD~1

# 2: This works every time
# git --git-dir $repopath/.git checkout -b somebranch HEAD~1

运行 命令行中的脚本将按预期工作,但是当您推送到 repo 时,挂钩将失败。注释 1 和取消注释 2 在这两种情况下都有效。

我找不到任何表明这是预期行为的文档 - 将不胜感激。

这是 git 2.7.4 Ubuntu 16.04。

文字上的区别:

git -C <em>目录git-sub-command ...</em>

和:

git --git-dir <em>目录git-sub-command ...</em>

是前端设置程序git使用了OS-level"change directory to"操作(os.chdir来自Python,chdir()来自C , 等)为第一个,并为第二个设置环境变量 $GIT_DIR 。在任何一种情况下,它都会找到 sub-command 并运行它。 (请注意,您实际上可以同时执行 。)This is documented,包括多个 -C 选项的效果以及 -C--git-dir.

然而,这只是将问题推低了一个层次:现在您需要知道 git-checkout(在 git --exec-path 目录中找到)与 $GIT_DIR 的不同之处与当前工作目录。直接答案在 the top level git command documentation, under the ENVIRONMENT VARIABLES section:

GIT_DIR
If the GIT_DIR environment variable is set then it specifies a path to use instead of the default .git for the base of the repository. The --git-dir command-line option also sets this value.

这就是的用武之地。当你写一个Git钩子时,你必须意识到Git可能会为你设置一些环境变量。如果 $GIT_DIR 设置为相对路径名,并且您没有覆盖它,并且您确实更改了目录,那么您将更改所有各种 Git sub-command 定位存储库的方式。因此,您必须 un-设置它(以获得默认的 $GIT_DIR-not-set 行为),或者显式地将它设置为绝对路径(以在整个过程中保留它目录更改),或将其显式设置为其他存储库的路径(相对或绝对),具体取决于您想要.

的行为

请注意 --work-tree 设置 $GIT_WORK_TREE,还有其他类似的变量,但是——至少在迄今为止的所有 Git 版本中——$GIT_DIR 是唯一的"pre-set for you"(或 "for your annoyance" :-))在 Git 钩子中。

如果您从那时起(2017 年)升级了 Git,请确保使用最新的 Git 2.35(2022 年第一季度):

"git rebase -x"(man) 在用 C 重写命令时错误地开始导出 GIT_DIRGIT_WORK_TREE 环境变量,已更正。

参见 commit 434e063 (04 Dec 2021) by Elijah Newren (newren)
(由 Junio C Hamano -- gitster -- in commit 57f28f4 合并,2021 年 12 月 21 日)

sequencer: do not export GIT_DIR and GIT_WORK_TREE for 'exec'

Signed-off-by: Elijah Newren
Acked-by: Johannes Schindelin
Acked-by: Johannes Altmanninger
Acked-by: Phillip Wood

Commands executed from git rebase --exec(man) can give different behavior from within that environment than they would outside of it, due to the fact that sequencer.c exports both GIT_DIR and GIT_WORK_TREE.
For example, if the relevant script calls something like

git -C ../otherdir log --format=%H --no-walk

the user may be surprised to find that the command above does not show a commit hash from ../otherdir, because $GIT_DIR prevents automatic gitdir detection and makes the -C option useless.

This is a regression in behavior from the original legacy implemented-in-shell rebase.
It is perhaps rare for it to cause problems in practice, especially since most small problems that were caused by this area of bugs has been fixed-up in the past in a way that masked the particular bug observed without fixing the real underlying problem.

An explanation of how we arrived at the current situation is perhaps merited.
The setting of GIT_DIR and GIT_WORK_TREE done by sequencer.c arose from a sequence of historical accidents:

  • When rebase was implemented as a shell command, it would call git-sh-setup, which among other things would set GIT_DIR -- but not export it.
    This meant that when rebase --exec commands were run via /bin/sh -c $COMMAND they would not inherit the GIT_DIR setting.
    The fact that GIT_DIR was not set in the run $COMMAND is the behavior we'd like to restore.

  • When the rebase--helper builtin was introduced to allow incrementally replacing shell with C code, we had an implementation that was half shell, half C.
    In particular, commit 18633e1 ("rebase -i: use the rebase--helper builtin", 2017-02-09, Git v2.13.0-rc0 -- merge listed in batch #1) added calls to exec git rebase--helper ...
    which caused rebase--helper to inherit the GIT_DIR environment variable from the shell.
    git's setup would change the environment variable from an absolute path to a relative one (".git"), but would leave it set.
    This meant that when rebase --exec commands were run via run_command_v_opt(...) they would inherit the GIT_DIR setting.

  • In commit 09d7b6c ("sequencer: pass absolute GIT_DIR to exec commands", 2017-10-31, Git v2.16.0-rc0 -- merge listed in batch #1), it was noted that the GIT_DIR caused problems with some commands; e.g. git rebase --exec 'cd subdir && 'git describe'(man) ...
    would have GIT_DIR=.git which was invalid due to the change to the subdirectory.
    Instead of questioning why GIT_DIR was set, that commit instead made sequencer change GIT_DIR to be an absolute path and explicitly export it via argv_array_pushf(&child_env, "GIT_DIR=%s", absolute_path(get_git_dir())); run_command_v_opt_cd_env(..., child_env.argv)

  • In commit ab5e67d ("sequencer: pass absolute GIT_WORK_TREE to exec commands", 2018-07-14, Git v2.19.0-rc0 -- merge), it was noted that when GIT_DIR is set but GIT_WORK_TREE is not, that we do not discover GIT_WORK_TREE but just assume it is '.'.
    That is incorrect if trying to run commands from a subdirectory.
    However, rather than question why GIT_DIR was set, that commit instead also added GIT_WORK_TREE to the list of things to export.

Each of the above problems would have been fixed automatically when git-rebase(man) became a full builtin, had it not been for the fact that sequencer.c started exporting GIT_DIR and GIT_WORK_TREE in the interim.
Stop exporting them now.