Frage

For a pre-commit hook that modifies the staged files, I need to figure out what has to be staged after the hook was run.

The pre-commit hook applies some pretty-printing on the files that should be committed. The hook does the following tasks:

  • replace tabs by spaces
  • remove trailing whitespace at the end of lines
  • remove double empty lines *)
  • if missing, add an empty line at the end of the file *)

Actions marked with *) are those that are causing the problem described below.

After these have been done, the hook adds the modified file(s) to the index using git add $filename. That way, the whole file gets staged and I'm no longer able to commit only small portions (i.e. hunks) of the modified file.

Of course, I could git add --no-verify and bypass the hook, but that option isn't available when using the git gui. Also, I want the took to apply pretty-printing on the staged lines of the file, so bypassing the hook is not my aim.

Is there a way to find out what has to be added to the index after the pretty-printing was applied, so that I could stage the right content after the hook was run instead of staging the whole file?


Edit 1:
While the answer provided by David Brigada looks promising, it doesn't work: git stash --keep-index leaves the staged changes intact (that's the good part), but (at least on msysgit) it puts all changes into the stash (not only the unstaged ones). This results in merge conflicts when popping the stash back into WC because the staged lines might get modified.


Edit 2:
Also the updated answer of David leads to no success since git refuses to merge the stash into a dirty WC.


Edit 3:
The answer from larsks points me to the usage of .gitattributes. On the first glimpse, this seems right but I find it quite confusing that the checked in version was run through a filter and the WC is different from the checked in version. At least, that was my experience and is backed up by the following remark in the Git Book:

If you commit those changes and check out the file again, you see the keyword properly substituted

I have to remove the file and then check it out again to see the changes that were applied by the filter? No way! Any more hints?

War es hilfreich?

Lösung

I'm not sure that a pre-commit hook is the right place to perform this sort of work. Git has a filtering mechanism that allows you to apply commit/checkout filters to documents via .gitattributes; the Pro Git Book includes examples that use this filtering mechanism to apply the indent program to C source files automatically.

Andere Tipps

You can put the changes that you have not staged for commit in a stash in your pre-commit script, and then pop them off when you are done. The --keep-index option only stashes changes that you haven't added to the index (haven't run git add on), and the --quiet options suppress the notices that you're creating and destroying the stash.

The way you described isn't quite the canonical "git way to do it," most pre-commit scripts that I've seen only check for incorrect style and then error out of the commit if they find anything. That way you don't have your pre-commit script inadvertently rewrite some file that was supposed to have tabs in it for some reason (e.g. testing data)---you could simply rerun the commit with --no-verify once you were confident that the errors were supposed to be there.

I have a simple pre-commit script here. It pretty much just runs git's basic test of diff-index --check, but it uses the git stash to only work on the changes that it should.

#!/bin/sh
if git rev-parse --verify HEAD >/dev/null 2>&1
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

git stash save --keep-index --quiet
git diff-index --check --cached $against --
RETVAL=$?
git stash pop --quiet
exit $RETVAL

Update:

You will get merge conflicts if you try to use this strategy with a script that modifies the contents you want to commit. We should take the stash pop out of the pre-commit script, and perform an explicit merge in the post-commit script.

#!/bin/sh

git stash save --keep-index --quiet
# Add your script to prettify the code here ...
git add .

Now the post-commit script looks like this

#!/bin/sh

git merge stash@{0} -s recursive -Xtheirs
git stash drop --quiet stash@{0}

The recursive merge with the theirs parameter should keep the changes the script made when possible.

I'm not 100% sure I understand the goal, but perhaps you could try the following approach. Have your hook generate a patch instead of writing the changes to disk. (You could do this, e.g., in Python using the difflib module, or write a temp file and shell out to diff.) Then use git apply (with --cached and other flags as appropriate) to apply your patch to the index as well as to the working tree (either manually or automatically by the hook).

Since my goal does not seem to be achievable, I helped me with another solution:

I only stage parts of files when I'm in the git gui. Luckily, the git gui lets you add custom commands to the Tools menu. I set up a script called hooks.sh that receives one parameter, either enable or disable. This script is added to the Tools menu, once with enable, once with disable as parameter.

That way, I could comfortable disable the hook when staging just a part of the file, then commit and then enable the hook again.

There you go:

#!/bin/sh

################################################################################
# hooks.sh                                                                     #
# enable or disable git hooks.                                                 #
################################################################################

THEHOOK=pre-commit                  # which hook to work with
ENABLEDFILE=./.git/hooks/$THEHOOK   # original name of the file
DISABLEDFILE=$ENABLEDFILE.disabled  # disabled name of the file
OLDFILE=''                          # for mv command: old name
NEWFILE=''                          # for mv command: new name

# parse parameters
case $1 in
  disable)
    OLDFILE=$ENABLEDFILE
    NEWFILE=$DISABLEDFILE
    ;;
  enable)
    OLDFILE=$DISABLEDFILE
    NEWFILE=$ENABLEDFILE
    ;;
  *)
    echo -e "operation:\n    $0 enable  to enable the hook\n    $0 disable to disable the hook"
    exit 1
    ;;
esac

if [ -e $OLDFILE ]
then
  mv $OLDFILE $NEWFILE
else
  echo "nothing to do"
fi

What you want is to have a pre-commit hook that does your described actions but then also a commit-msg hook.

This is how I would solve the problem:

  1. Add the following pre-commit hook:

    #!/bin/sh
    git diff --cached --name-only -z --diff-filter=ACM |
            xargs -r0 YOUR_SMUDGE_SCRIPT
    git diff --cached --name-only -z --diff-filter=ACM |
            xargs -r0 git add -u -v --
    
  2. Add the following commit-msg hook:

    #!/bin/sh
    awk '!/^[[:space:]]*(#|$)/{exit f++}END{exit !f}' "$1" && exit
    # NOTREACHED unless commit was aborted
    git diff --cached --name-only -z --diff-filter=ACM |
            xargs -r0 YOUR_CLEAN_SCRIPT
    git diff --cached --name-only -z --diff-filter=ACM |
            xargs -r0 git add -u -v --
    

Taking care to replace YOUR_CLEAN_SCRIPT and YOUR_SMUDGE_SCRIPT with the command-name you want to run that will do the actual clean/smudge routines. The smudge routine should expand keywords (occurring pre-commit) and the clean routine should undo the changes (occurring when a commit gets aborted). These scripts will be passed the files that need to be modified (previously staged for a different edit).

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top