git pre-commit hook to format and re-add files at the same time
Asked Answered
U

3

15

We're currently using a git hook (below) to run astyle on our source code before allowing the user to commit. This has the caveat that the user must commit, have their code formatted, then commit again which is a bit of a nuisance. Ideally we'd want the hook to format the code and then include that formatted code in the original commit instead. I've tried re-adding the changed files but it causes ref errors (obviously). I've also tried getting the history in the pre-commit hook and trying to exit the hook and re-run the git commit command with no luck.

# Run astyle on changed .cs files, ignoring $ignored
res=$(exec git diff --cached --name-only | \
    grep -Ev $ignored | \
    xargs astyle --options=conf/astylerc | \
    tail -n 1)
num_formatted=$(echo $res | cut -b 1) # We are only interested in the number preceeding 'formatted'.
[[ $num_formatted -ne 0 ]] && echo "WARNING: Code has been automatically formatted. Please re-add and re-commit" && exit 1 || echo "No code to format! Continuing commit"

Does anyone have any ideas?

Unsettle answered 25/6, 2015 at 18:2 Comment(2)
Why not run a pre-commit hook and format code before committing?Peper
It is a pre-commit hook. It's just that my devs need to run git add and then git commit again after the files have been formatted. So it looks like: git add file.cs git commit -m "msg" WARNING: Code Formatted git add file.cs git commit -m "msg again" which is annoying. I'd prefer: git add file.cs git commit -m "msg" Code Formatted, Re-Added, and Commit! Thanks!Unsettle
P
12

In your pre-commit hook, you need to add your files, so if your hook is something like:

#!/bin/bash
echo 1 > file
exit 0

then you would need to modify it to have the add:

#!/bin/bash
echo 1 > file
git add file
exit 0

To get a list of all modified files, you could use git-ls-files:

git ls-files -m

However, it would be better if you could just get a list from your code of which files are modified or just add all files again. git diff-tree -r --name-only --no-commit-id <tree-ish> should work for you to get a list of all files.

Basically, adding the files again after modifying works because the commit does not occur until after your pre-commit hook runs, so whatever is staged in the working tree at that point is committed.

Peper answered 25/6, 2015 at 22:5 Comment(2)
Wow you're so right. I have no idea why I tried committing from the hook... One of those times you get blinded by over-complicating the problem. Anyway, now I run the formatter on all files that require it, store it in a $changed variable, and then git add $changed. Works a charm! Thanks!Unsettle
You could also use git add -u to add all the modified files.Wozniak
M
11

Edited following this answer.

You can format and add your files back within the hook. The problem is that you might have unstaged modifications of staged files. To do this in a clean way you can get the file from index as a tmp, format the tmp and replace the entry in index using the formatted tmp. Here is an approach to something that should solve the problem:

# Regexp for grep to only choose some file extensions for formatting
exts="\.\(ext\|ext2\)$"

# The formatter to use
formatter=`which your_formatter`

# Check availability of the formatter
if [ -z "$formatter" ]
then
  1>&2 echo "$formatter not found. Pre-commit formatting will not be done."
  exit 0
fi

# Format staged files
git diff --cached --name-only --diff-filter=ACMR | grep "$exts" | while read file; do
  echo "Formatting $file"
  # Get the file from index
  git show ":$file" > "$file.tmp"
  # Format it
  "$formatter" -i "$file.tmp"
  # Create a blob object from the formatted file
  hash=`git hash-object -w "$file.tmp"`
  # Add it back to index
  git update-index --add --cacheinfo 100644 "$hash" "$file"
  # Remove the tmp file
  rm "$file.tmp"
done

# If no files left in index after formatting - fail
ret=0
if [ ! "`git diff --cached --name-only`" ]; then
  1>&2 echo "No files left after formatting"
  exit 1
fi
Marcello answered 13/3, 2018 at 20:7 Comment(2)
This script leaves the file in the file system unformatted which makes diffs still present the unformatted code after the commit. I added: "$formatter" -i "$file" after the line that formats the .tmp. There's almost certainly a better way to address the issue than this though.Liguria
git cat-file -p "$hash" >"${file}"Hypocoristic
A
4

You can stash just the unstaged changes using the technique described here. Then run the formatter on just the staged changes and pop the stash. Below pre-commit hook uses clang-format-diff

#!/bin/sh

# stash unstaged changes
git commit --no-verify -m 'Save index'
old_stash=$(git rev-parse -q --verify refs/stash)
git stash push -m 'Unstaged changes'
new_stash=$(git rev-parse -q --verify refs/stash)
git reset --soft HEAD^

# format staged changes
git diff -U0 --no-color --staged HEAD -- '*.java' | $PWD/clang-format-diff.py -i -p1

git add -u
if [ "$old_stash" != "$new_stash" ]; then # if unstaged changes were stashed reapply to working tree
    git stash pop
fi
exit 0
Augustina answered 2/2, 2021 at 13:31 Comment(2)
In order to be able to amend the summary of the previous commit with git commit --amend, I had to add --allow-empty to the first command.Elum
This script will fail when stash is empty.Seedling

© 2022 - 2024 — McMap. All rights reserved.