Suppose git status
gives this:
# On branch X
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: file1.cc
# modified: file1.h
# modified: file1_test.cc
# modified: SConscript
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
# (commit or discard the untracked or modified content in submodules)
#
# modified: file1.cc
# modified: tinyxml2 (untracked content)
#
In this case, only some of the changes made to file1.cc have been staged/indexed for the next commit.
I run a pre-commit script to do run a style checker:
#!/bin/bash
git stash -q --keep-index
# Do the checks
RESULT=0
while read status file
do
if python ~/python/cpplint.py "$file"; then
let RESULT=1
fi
done < <(git diff --cached --name-status --diff-filter=ACM | grep -P '\.((cc)|(h)|(cpp)|(c))$' )
git stash pop -q
[ $RESULT -ne 0 ] && exit 1
exit 0
As suggested by here, I stash unstaged files before I run my style checks, and pop them afterwards. However, in the case where only some of the changes in a file were staged, this causes a merge conflict when I pop the stash at the end of the pre-commit hook.
What is a better way to do this? I want to run a style check against the staged version of the files about to be committed.
I'd avoid using git stash
automatically in hooks. I found that it is possible to use git show ':filename'
to get contents of stashed file.
Instead I used next approach:
git diff --cached --name-only --diff-filter=ACMR | while read filename; do
git show ":$filename" | GIT_VERIFY_FILENAME="$filename" verify_copyright \
|| exit $?
done \
|| exit $?
Replace git stash -q --keep-index
with:
git diff --full-index --binary > /tmp/stash.$$
git stash -q --keep-index
...and git stash pop -q
with:
git apply --whitespace=nowarn < /tmp/stash.$$` && git stash drop -q
rm /tmp/stash.$$
This will save the diff to a temporary file and reapply it using git apply
at the end. The changes are still redundantly saved to the stash (which is later dropped), so that if something goes wrong with the reapplying, you can inspect them using git stash show -p
without having to look for the temp file.
How to gate commits without stashing
We use this .git/hooks/pre-commit
for checking an atom syntax package
Key bits
git checkout-index -a --prefix={{temp_dir}}
It may/may not be far slower/take more space than stashing, but automation which messes with the index seems inherently brittle. Perhaps there needs to be a git contrib script which makes a soft-/hard-link tree, minimum space read-only, temporary index checkout, facilitating better/faster .git/hooks/pre-commit
(or, say, .git/hooks/pre-commit-index
) scripts so that a full second copy of the working dir isn't needed, just the working-dir->index changes.
#!/usr/bin/env ruby
require 'tmpdir'
autoload :FileUtils, 'fileutils'
autoload :Open3, 'open3'
autoload :Shellwords, 'shellwords'
# ---- setup
INTERACTIVE = $stdout.tty? || $stderr.tty?
DOT = -'.'
BLOCK_SIZE = 4096
TEMP_INDEX_DIR = Dir.mktmpdir
TEMP_INDEX_DIR_REAL = File.realpath(TEMP_INDEX_DIR)
def cleanup
FileUtils.remove_entry(TEMP_INDEX_DIR) if File.exist? TEMP_INDEX_DIR
end
at_exit { cleanup }
%w[INT TERM PIPE HUP QUIT].each do |sig|
Signal.trap(sig) { cleanup }
end
# ---- functions
def fix_up_dir_output(data)
data.gsub! TEMP_INDEX_DIR_REAL, DOT
data.gsub! TEMP_INDEX_DIR, DOT
data
end
def sh(*args)
Open3.popen3(*args) do |_, stdout, stderr, w_thr|
files = [stdout, stderr]
until files.empty? do
if ready = IO.select(files)
ready[0].each do |f|
begin
data = f.read_nonblock BLOCK_SIZE
data = fix_up_dir_output data
if f.fileno == stderr.fileno
$stderr.write data
else
$stdout.write data
end
rescue EOFError
files.delete f
end
end
end
end
if !(done = w_thr.value).success?
exit(done.exitstatus)
end
end
end
def flags(args)
skip = false
r = []
args.each do |a|
if a[0] == '-' && !skip
a.slice! 0
if a[0] == '-'
skip ||= a[1].nil?
a.slice! 0
r << a unless a.empty?
else # -[^-]+
r += a.split ''
end
end
end
r
end
def less_lint
args = %w[lessc --lint]
args << '--no-color' unless INTERACTIVE
args << 'index.less'
sh(*args)
end
def ensure_git_commit_signed
pcmd = `ps -wwp#{Process.ppid} -ocommand=`.chop
args = flags(Shellwords.split(pcmd)[1..-1])
return unless (args & %w[gpg-sign S]).empty?
$stderr.puts 'All git commits must be GPG-signed'
$stderr.puts " command: #{pcmd}"
exit 1
end
# ---- main
# 1. make sure all commits are signed
ensure_git_commit_signed
# 2. check files that are in the index
sh 'git', 'checkout-index', '-a', "--prefix=#{TEMP_INDEX_DIR}/"
Dir.chdir TEMP_INDEX_DIR do
# 3. make sure all commits contain only legal .less files
less_lint
end