1

How would you write a pre-push hook that rejects pushing when the overall diff introduces a specific word/sentence/string?

Motivating use-case:

  1. committing TODO notes (marking tasks to complete before pushing)
  2. letting git prevent me from forgetting to address said notes when pushing

Notes:

  1. I have seen a lot of different questions and answers but none come close. The ones that try quickly fail on edge cases (e.g. this and this).
  2. notice that "overall diff" means that commits adding TODO would be allowed as long as the string was removed in subsequent commits being pushed (such that the overall delta did not contain it)
  3. the main difficulty is finding a range to pass to git diff that works in all cases.
  4. only additions of TODO are to be blocked, removals should be allowed in the diff
  5. modifications of lines previously containing TODO that maintain it, imply a TODO addition (even if simultaneous with a removal) and should therefore be blocked (rationale: no objective way to distinguish whether the introduced TODO is the same as the removed one).
  6. such a hook should cope with all valid pushes, checking only deltas corresponding to ranges of new commits (e.g. nothing to check in push --delete). Some particular cases to consider:
    • new branches
    • deleted branches
    • renamed branches
    • branching off of something other than master (so no merge-base origin/master)
    • splits/merges
    • tags
    • force pushes
  7. Bonus variation: prevent pushing any commits adding TODO (rather than in the overall diff).
ricab
  • 2,697
  • 4
  • 23
  • 28

1 Answers1

1

Could try something like the following.

#!/usr/bin/env ruby

branchName = `git rev-parse --abbrev-ref HEAD`.strip
log = `git reflog show --no-abbrev #{branchName} --format='\%h'`.split("\n")
range = "#{log.last}..#{log.first}".gsub("'", "")

`git diff --name-only --diff-filter=ACMR #{range}`.each_line do |file|
  file = file.chomp
  command = "git show #{log.first}:#{file}".gsub("'", "")
  content = `#{command}`

  if ( content =~ /TODO/ )
    puts "'#{file}' contains TODO"
    exit 1
  end
end

exit 0

This will search the content of any new, added, modified, or renamed files for the word TODO. If it finds the word it will exit and output the name of the file that contains the matching regex.

This will work for new branches, but it does have issues if you rebase your branch as in that case it may pull in other people's changes.

user5754
  • 147
  • 1
  • 7
  • Hmm, I don't think reflog cuts it. If you branched off of a branch that is itself not pushed, the reflog command would just omit all those earlier unpushed commits – ricab Feb 05 '19 at 18:31
  • True... I guess you could compare your HEAD to the base branch ("master" for example) if you want to check all changes not just commits on your current branch. – user5754 Feb 07 '19 at 03:04
  • The point is that knowing the commit range that is going to be pushed is the main part of the question. Comparing to master is not ideal... two reasons off the top of my head: 1) there may be no "master" with that name; 2) your branch may be based off of something more recent that was already pushed. – ricab Feb 07 '19 at 10:07