git pre-commit hook code formatting with partial c

2019-02-17 01:38发布

is there a way to have a pre-commit hook which auto-formats the code (for example with astyle) but does not destroy a partial commit?

Workflow:

# 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

My problem is, that if you do a git add inside the pre-commit hook, which is required after the script formatted the source code, adds the "another" chunk, too. But I don't want that.

Is there a way to achieve this?

2条回答
唯我独甜
2楼-- · 2019-02-17 02:11

I'd do this by doing the work with the low-level "plumbing" commands, my first attempt would be something along the lines of

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

To avoid redundant processing, start from git diff-index --name-only --diff-filter=AM output as @torek suggests.

查看更多
在下西门庆
3楼-- · 2019-02-17 02:15

There is (sort of) a way to do it. I wouldn't, but if you really want to, proceed along these lines.

First, you need to separate out the two items you have now:

  • the staged changes
  • the not-staged work tree items

Moreover, you want the first set to be available for reformatting.

This can be done with git stash, as I showed in the answer to How do I properly git stash/pop in pre-commit hooks to get a clean working tree for tests? (see the warning in there about a bug in git stash too, though).

Basically, you want to reach the point in the script there where the tests get run:

# Run tests
status=...

Once in this state, you can run work-tree items through formatters, and git add the result in a pre-commit hook (as you already discovered). This will avoid formatting the "another" chunk since it was not in the work directory version. You can then let the commit proceed (i.e., the rest of the script does not apply here).

The problem is now restoring the work-tree version from the stash. Because you modified the index, you can't go back to this, even after the commit finishes:

# Restore changes
git reset --hard -q && git stash apply --index -q && git stash drop -q

Instead, what you want is to find the diff between the stashed index (stash^1) and the stashed work-tree (stash), and apply that to the new HEAD commit. There are at least two ways to do this without using git plumbing commands. Both are likely to result in conflicts due to the reformatting of the committed version:

  1. git diff stash^1 stash | git apply --reject (and eventually 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

Method 1 is simpler but messy, as changes that would have merge conflicts get dropped into "reject" files. Method 2 uses the merge machinery, so changes get conflict markers instead, if needed. (If there are no conflicts, the -n prevents a commit so that you can do your own with a real message, instead of copying the dummy for-cherry-pick message.)

Of course I have not tested any of this. Also, there are methods for doing this without using git stash, such as checking out the index versions of the git add-ed files into a separate directory, formatting things there, and then adding the formatted versions back, so that none of this process affects the current work directory. This is probably superior anyway, if you're really determined to do this. Here's an script for that method (also not really tested—it needs a bit of robustness added, using -z and xargs -0 perhaps, to handle file names containing white space, in the checkout of diff-index output section):

# 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 .

Here's what I would recommend instead though: rather than formatting the code at that point in the pre-commit hook, simply check whether it is formatted. If so, permit the commit. If not, reject it. This is much more in the spirit of a pre-commit hook, and it allows using the script in that other answer. Basically, at the point where it says:

status=...

you simply run something that checks whether the formatter would change anything (perhaps by allowing the formatter to do its thing, and seeing if the result is different from what's in the to-be-committed index). That gets you your status. Then you do the rest of what's in the script, with the git reset --hard -q && git stash apply --index -q && git stash drop -q restoring everything to the way it was when the stash was created.

查看更多
登录 后发表回答