使用Git预提交钩子进行部分提交的代码格式化?

7

有没有一种方式可以使用预提交钩子自动格式化代码(例如使用astyle),但不会破坏部分提交?

工作流程:

# edit a file.txt
git add -p file.txt
# add one chunk, but not another

git commit -m 'a message'
[PRE_COMMIT_HOOK] Formatting source code

git status
# the "another" chunk is still not added

我的问题是,在脚本格式化源代码后,如果在pre-commit hook中执行git add命令,它也会添加“another”块,但我不希望如此。有没有办法实现这个需求?
2个回答

4
我会使用低级的“管道”命令来完成这项工作,我的第一次尝试将是以下内容:
git ls-files --stage \*.c | while read mode object stage path; do
case $mode in
10*)
      formatted=`git show $object | indent | git hash-object -w --stdin`
      git update-index --cacheinfo $mode $formatted "$path"
;;
esac
done

为了避免冗余处理,建议像 @torek 建议的那样从 git diff-index --name-only --diff-filter=AM 输出开始。

“--staged” 应该改为 “--stage”,而 “--cache-info” 应该改为 “--cacheinfo”。 - musicmatze
现在呢?我猜工作区在那之后需要进行更新,对吧?编辑:但是目前看起来真的很不错! - musicmatze
啊,下一步是通过indent程序将“真实”文件进行管道传输。这是一个解决方案,但我不太喜欢它。我希望你能理解我认为缺少的东西:由indent程序编辑的行在存储库中被更改,但在工作区中没有更改。如果我通过indent传输真实文件,我应该得到与预期相同的结果。 - musicmatze
1
修正了拼写,谢谢。对于复制工作树副本,您的编辑器是否已经设置好每次都这样做呢?这就是我会这样做的方式,我认为这是add --patch选项损坏的备份,你不能真正期望以那种方式捕获它,如果没有这个,我会和@torek一样:“而不是在pre-commit钩子中格式化代码,在那个点上只需检查它是否被格式化。如果是这样,请允许提交。” - jthill

1
有一种(有点)方法可以做到。我不建议这样做,但如果你真的想要,可以按照以下步骤进行。
首先,你需要将现有的两个项目分开:
- 暂存更改 - 未暂存的工作树项目
此外,你希望第一组可用于重新格式化。
可以使用git stash完成这个操作,就像我在如何在提交前钩子中正确地git stash/pop以获取干净的工作树进行测试?的答案中展示的那样(注意其中关于git stash中的错误警告)。
基本上,你希望在脚本中运行测试的那个点。
# Run tests
status=...

一旦进入这个状态,您可以通过格式化运行工作树项目,并在预提交钩子中使用git add结果(就像您已经发现的那样)。这将避免格式化“another”块,因为它不在工作目录版本中。然后您可以让提交继续进行(即脚本的其余部分在此处不适用)。
问题现在是从备份中恢复工作树版本。由于您修改了索引,所以即使提交完成后也无法回到此版本:
# Restore changes
git reset --hard -q && git stash apply --index -q && git stash drop -q

相反,你想要的是找到存储的索引(stash^1)和存储的工作树(stash)之间的差异,并将其应用于新的HEAD提交。有至少两种方法可以做到这一点,而不使用git plumbing命令。由于提交版本的重新格式化,两者都可能导致冲突:
  1. git diff stash^1 stash | git apply --reject (最终要使用git stash drop
  2. git stash branch tempbranch; git commit -m for-cherry-pick; git checkout prev-branch; git cherry-pick -n tempbranch; git branch -D tempbranch
方法1更简单但混乱,因为会将可能导致合并冲突的更改放入“拒绝”文件中。方法2使用合并机制,因此如果需要,更改会获得冲突标记。如果没有冲突,则-n会阻止提交,以便您可以使用真正的消息自己进行提交,而不是复制虚拟的for-cherry-pick消息。

当然我没有测试过这些方法。此外,还有一些不使用git stash的方法来完成这个任务,例如将git add的文件的索引版本检出到一个单独的目录中进行格式化,然后添加格式化后的版本,以便整个过程不会影响当前的工作目录。如果你真的决心要这样做,这可能更好。以下是一个脚本用于实现那种方法(也没有经过真正的测试——它需要加入一些健壮性,例如使用-zxargs -0来处理包含空格的文件名,在checkout diff-index输出部分):

# make a directory for formatting files
WORKDIR=$(mktemp -d -t reformat) || exit 1
# clean it up when we leave
trap "rm -rf $WORKDIR 0 1 2 3 15"
# for files Added or Modified in the index, copy them to $WORKDIR
git --work-tree=$WORKDIR checkout -- \
    $(git diff-index --cached --name-only --no-renames --diff-filter=AM HEAD)
# reformat files in the work-dir
(cd $WORKDIR; ...)
# for each file in the work-dir, re-"add" that version to this tree
# (this assumes the reformatter did not leave extraneous files!)
git --work-tree=$WORKDIR add --ignore-removal .

这里我建议的做法是:不要在 pre-commit hook 中对代码进行格式化,而是检查 它是否已经 格式化。如果已经格式化了,就允许提交。如果没有格式化,则拒绝提交。这更符合 pre-commit hook 的精神,并且可以使用 其他答案中的脚本。基本上,在它说的那个地方:

status=...

你只需运行一个检查格式化器是否会有任何更改的程序(例如,允许格式化器执行其操作,并查看结果是否与要提交的索引中的内容不同)。这样就可以获得状态。然后按照脚本中的其余部分进行操作,使用"git reset --hard -q && git stash apply --index -q && git stash drop -q"将所有内容恢复到创建存储时的状态。

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