Git pre-commit钩子:已更改/添加的文件

97

我正在编写一个 pre-commit hook。 我想对所有扩展名为 .php 的文件运行 php -l 命令。但是我被卡住了。

我需要获取已暂存的新/更改文件列表,应该排除已删除的文件。

我已经尝试过使用 git diffgit ls-files,但我认为我需要一些帮助。


http://phpadvent.org/2008/dont-commit-that-error-by-travis-swicegood - Maerlyn
这很不错。然而它无法处理部分暂存的文件。请看我对@LarryH答案的评论。 - igorw
6个回答

116

获取相同列表的稍微更整洁的方式是:

git diff --cached --name-only --diff-filter=ACM

这将返回需要检查的文件列表。

但仅运行php -l在你的工作副本上可能不是正确的做法。如果你执行部分提交,即只选择当前工作集与提交HEAD之间差异的子集,则测试将在你的工作集上运行,但将认证一个从未存在于你的磁盘上的提交。

要做到正确,你应该将整个暂存的图像提取到临时区域并在那里执行测试。

rm -rf $TEMPDIR
mkdir -p $TEMPDIR
git checkout-index --prefix=$TEMPDIR/ -af
git diff --cached --name-only --diff-filter=ACM | xargs -n 1 -I '{}' \bin\echo TEMPDIR/'{}' | grep \\.php | xargs -n 1 php -l

参考构建更好的Git预提交钩子了解另一种实现方法。


4
实际上可以将文件内容传输到 php -l。这就是我们最终采用的方法。请参见此处:http://github.com/phpbb/phpbb3/blob/develop-olympus/git-tools/hooks/pre-commit - igorw
2
要检查暂存文件的语法,您可以使用 git show :FILENAME | php -l - Aad Mathijssen
13
建议将 --diff-filter 参数改为 "ACMR",因为重命名的文件(R)也可能有更改。 - Droopycom

57

git diff --cached --name-status会显示暂存区中的内容摘要,因此您可以轻松地排除已删除的文件,例如:

git diff --cached --name-status 将展示已暂存文件的总结信息,这样您就可以轻松排除已删除的文件,例如:

M       wt-status.c
D       wt-status.h

这表示 wt-status.c 被修改了,wt-status.h 在暂存区(索引)中被删除。因此,要检查仅未被删除的文件:

steve@arise:~/src/git <master>$ git diff --cached --name-status | awk '$1 != "D" { print $2 }'
wt-status.c
wt-status.h

如果文件名中包含空格,你需要额外地处理一些问题(使用 git diff 的 -z 选项以及更为复杂的解析)。


谢谢,这是一个好的开始。但是,如果我更改了一个文件而没有将其暂存,它仍然会显示出来。我正在运行git版本1.7.0.1.147.g6d84b(最近的自定义构建)。不确定这是否是预期行为。 - igorw
那听起来很奇怪。"--cached"开关应该只显示已经暂存的文件:虽然我正在使用1.6.5进行测试,但这似乎令人惊讶会发生改变... "git diff --cached"本身是否显示未暂存的更改? - araqnid
经过一些调试,我能够追溯到另一个原因。非常感谢! - igorw
@igorw,我很感兴趣,但链接失效了。 - Simon
请注意,如果您只想要文件的名称,可以使用--name-only而不是--name-status。这样可以避免额外的awk操作。 - L.P.
2
如果文件名中不包含空格,则 awk 命令的 print $2 部分才能正确运行。解决方法之一:... | awk '$1 != "D" { $1=""; sub(FS,""); print $0 }'。 另一个注意点:对于文件重命名,git diff 命令会打印旧名称和新名称。如果只需要新名称,请向 git diff 添加 --no-renames 选项,这将把重命名视为删除旧文件并添加新文件。 - Gene Pavlovsky

27

这里提供的回答都不支持包含空格的文件名。最好的方法是结合xargs -0命令使用-z标志。

git diff --cached --name-only --diff-filter=ACM -z | xargs -0 ...

这是 Git 内置示例中所提供的内容(请参阅.git/hooks/pre-commit.sample)。


17

这是我用于Perl检查的代码:

#!/bin/bash

while read st file; do
    # skip deleted files
    if [ "$st" == 'D' ]; then continue; fi

    # do a check only on the perl files
    if [[ "$file" =~ "(.pm|.pl)$" ]] && ! perl -c "$file"; then
        echo "Perl syntax check failed for file: $file"
        exit 1
    fi
done < <(git diff --cached --name-status)

对于 PHP,它将看起来像这样:

#!/bin/bash

while read st file; do
    # skip deleted files
    if [ "$st" == 'D' ]; then continue; fi
    # do a check only on the php files
    if [[ "$file" =~ ".php$" ]] && ! php -l "$file"; then
        echo "PHP syntax check failed for file: $file"
        exit 1
    fi
done < <(git diff --cached --name-status)

2
非常好,但对于部分暂存文件无法工作,因为它会读取整个文件。 - igorw
谢谢!我修改了你的代码,并在done后面加入<<<$(git diff --cached --name-status),而不是使用管道,这样循环中就不会启动子shell。它允许在循环中更新变量以便稍后使用。提交答案更新供审核。最好的。 - lcetinsoy
无法再次编辑我的评论,因此语法实际上是'<< $(command)',类似于https://dev59.com/yGs05IYBdhLWcg3wLfAj#7390610。 - lcetinsoy

2

如果使用 -a 标志指定了提交调用,则 git diff --cached 不足够,并且无法确定该标志是否已在钩子中抛出。如果提交的参数可以供钩子检查,那么这将有所帮助。


git diff --cached 看起来是足够的。然而,我认为如果你在钩子中运行git status --porcelain处理的所有文件都不会在输出的第一个位置具有空格或问号。我还没有完全测试过,但到目前为止,在我的 repo 中,一个由新文件、已添加文件、修改文件组成的混合体,在我尝试提交显式文件、默认集文件、-a 代表全部文件的情况下,它一直保持着稳定。那么为什么使用 git status 而不是 git diff?我认为它更容易解析。 - mpersico
git status --porcelain | grep -E -v '^[? ]' - mpersico
git status --porcelain | perl -ane 'print $F[1],qq(\n) if m/^[ACM] /' 是更好的答案。它具有使用 --porcelain 选项的优点,保证永远不会改变。如果 Perl 对您来说太重了,请使用自己的解析器。 - mpersico

1
为了了解特定文件夹中的文件是否已更改,我会这样做:

modifiedFrontendFiles=$(git diff --cached --name-status --relative=frontend)

if [ -n "$modifiedFrontendFiles" ]; then
    npm run lint
    npm run lint-css
    npm run format
    git add .
fi

在我的情况下,我检查更改是否在前端文件夹中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接