How can I run git pre-commit checks only on staged

2019-04-04 07:35发布

问题:

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.

回答1:

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 $?


回答2:

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.



回答3:

How to gate commits without stashing

We use this .git/hooks/pre-commit for checking an atom syntax package

Key bits

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