34

I have a problem with the 'update' hook. In the case of a new branch, it gets a 0000000000000000000000000000000000000000 as the 'oldrev'. And I don't know how to handle that case.

We have the requirement, that every commit message references a valid Jira issue. So I have installed an "update" hook on our central repository. That hook gets an "oldrev" and a "newrev". I then pass those to "git rev-list" like this:

git rev-list $oldrev..$newrev

This gives me the list of all revs, which I can then iterate through, and do whatever I need to do.

The problem is, when the user pushes a new branch, the hook gets 0000000000000000000000000000000000000000 as the oldrev. And "git rev-list" simply complains with:

fatal: Invalid revision range 0000000000000000000000000000000000000000..21bac83b2

So how do I get the list of all the revs that are on that new branch? I have searched the net for quite some time now, and found nothing. The example hooks I found either

  • don't handle the problem, and fail with the above error message
  • incorrectly try to fix the problem by setting the oldrev to "", which returns the wrong results from rev-list
  • simply give up when they encounter that oldrev

None of these sound particularly exciting.

So does someone have any idea how to get the right answer in that case? I was thinking about querying git for "give me all revs that are reachable from newrev, but not from any of the other branches (=all branches except the new one)". But even that would give the wrong answer if there had been a merge from the new branch to any of the old ones.

marc.guenther
  • 2,761
  • 2
  • 23
  • 18
  • Possible duplicate of [View commits on a new branch in the update hook](https://stackoverflow.com/questions/18723219/view-commits-on-a-new-branch-in-the-update-hook) – Eugen Konkov May 28 '18 at 12:48

4 Answers4

18

The term "right answer" is a bit ambiguous in this case. I actually think that "all revs reachable from newrev but nowhere else" is completely correct. This is true even if there was a merge - in that case, you should see the commits unique to the new ref, and the merge commit, but not the commits that were merged.

So, I would say, check if the "oldrev" is all zeroes, and if it is, act accordingly:

if [ "$oldrev" -eq 0 ]; then
    # list everything reachable from newrev but not any heads
    git rev-list $(git for-each-ref --format='%(refname)' refs/heads/* | sed 's/^/\^/') "$newrev"
else
    git rev-list "$oldrev..$newrev"
fi
Matvey Aksenov
  • 3,802
  • 3
  • 23
  • 45
Cascabel
  • 479,068
  • 72
  • 370
  • 318
  • 2
    I'm not sure if it's something strange with my environment or an update to git, but the negation is removing refs on the current branch. I'm having to do something like this: `git rev-list $(git for-each-ref --format='%(refname)' "refs/heads/*" | grep -v '$ref' | sed 's/^/\^/') "$newrev"` – mmalone Nov 08 '12 at 01:47
  • 1
    For use in `update` (and `pre-receive`) hooks, as specified in the question, these answers are all more complicated and expensive. Joseph's answer below is the simplest and most efficient; that should be the chosen answer IMO. – MadScientist Jun 01 '14 at 19:41
  • @mmalone you are right. I had to add " | grep -v '$rev' ". Otherwise I always got empy result – dritan Mar 31 '15 at 09:18
  • Should `[ "$oldrev" -eq 0 ]` be used here? Most hashes contain letters leading to error messages like `bash: [: 95ce2ed9137143d1d8165355aa41a6311b799994: integer expression expected`. A regular expression seems to be more robust: `if echo "$oldrev" | grep -Eq '^0+$'; then …` – Martin Aug 02 '18 at 12:33
13

When $oldrev is all zeros, a different git rev-list command does all you need:

git rev-list $newrev --not --branches=*

will give you a list of revisions reachable from $newrev but not from any branches.

Note that this definitely does not do the same thing as git rev-list $oldrev..$newrev when oldrev is not all zeros, so you'll want to check which case you're in and pick the appropriate command to run.

Cascabel
  • 479,068
  • 72
  • 370
  • 318
Joseph
  • 189
  • 3
  • 2
  • 2
    Although actually this does work _in pre-receive and update hooks_ because when those hooks are run the new branch is not yet there! Nice! – MadScientist Jun 01 '14 at 19:35
  • Won't this also get you refs from before oldrev though, in the case where it's not all zeros? – Cascabel Jun 01 '14 at 21:40
  • 1
    @Jefromi what do you mean? The above command will give you a list of all commits which are not referenced from any branch at all. In update and pre-receive triggers, that's the new commits which are being added as part of the push: the branch refname either doesn't exist (if new) or hasn't been moved (if existing). This doesn't work in a post-receive hook but that that's not what the question was about. I don't understand why this is downvoted: it's the simplest, cleanest answer TO THE QUESTION ASKED. – MadScientist Jun 19 '14 at 16:01
  • @MadScientist This has the correct behavior if the user is pushing a new branch (in which case oldrev is all zeros); it gets you a list of commits which are not on any branch but reachable from newrev. But if the user is pushing an update to an existing branch, then the desired output is simply `git revlist oldrev..newrev`. So as far as I can tell, this isn't a drop-in replacement: you need to check whether the branch is new (oldrev is all zeros) and pick which command to run based on that. – Cascabel Jun 19 '14 at 16:42
  • @Jefromi: first the question is specifically about new branches, so this is still the best answer to the question asked and should not be downvoted. Second, while you may want to choose a different method in the existing branch case, this method does still work there and will do exactly what I said above: return the commits reachable from `$newrev` which are not present in any branch. Whether that's what you want or not depends on many things: a simple `rev-list` is not always right. Since that wasn't the question asked we can't know. – MadScientist Jun 19 '14 at 17:02
  • @MadScientist I agree that this is the right answer for the new branch case. It definitely doesn't work in the existing branch case - it gives you a bunch of commits that *aren't* on your existing branch. (And it's clear from the question that oldrev..newrev is what's desired, for existing branches.) I'll just edit this answer and avoid the issue. – Cascabel Jun 19 '14 at 17:13
  • @Jefromi it doesn't do the same as `rev-list`, but it _does_ work for existing branches _if what you want to find are the commits reachable from `$newrev` which are not in any existing branch_. I tried this in an update hook on an existing branch push and it worked as described. You must use an update or pre-receive hook to test this (or fake it with `update-ref` or similar). For existing branches a simple `rev-list` is not always right either: for example you might want to add `--ancestry-path`... it depends on what you need to do! – MadScientist Jun 19 '14 at 17:29
  • @MadScientist I understand that there are a lot of things that you might want to do - but given that newrev might be on an existing branch, causing this command to return nothing, I figure a warning is in order. I'm just trying my best to acknowledge what the OP asked for in the question, which included stating (or at least strongly implying) that for existing branches, oldrev..newrev was desired behavior. Feel free to edit further, it's not my post. – Cascabel Jun 19 '14 at 17:34
  • @Jefromi in what situation could newrev be on an existing branch? I can't come up with one. – MadScientist Jun 19 '14 at 17:37
  • @MadScientist Release branch lagging behind dev branch, pushing a fast-forward to the release branch? Thought these hooks were run whenever a ref is updated, not just when new objects had to be added to point the ref at. – Cascabel Jun 19 '14 at 17:45
  • @Jefromi yes I suppose. Again it depends on what you want to do. I would imagine that the reason you want to `rev-list` in an update hook is to get a list of commits which are being added to the repository so you can check them in some way before allowing the push. In that situation you wouldn't _want_ to get those commits since they would have already been checked last time. But, I can see situations where you _would_ want to do them again (maybe they're OK for one branch but not for the other or something). Anyway, good discussion! – MadScientist Jun 19 '14 at 21:14
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/55943/discussion-between-jefromi-and-madscientist). – Cascabel Jun 19 '14 at 21:46
  • 1
    The ordering is very important. If you change it to put `$newrev` after the options -- `git rev-list --not --branches=* $newrev` -- it will not work as the `--not` will apply to `$newrev` as well. So either leave it as-is or add another `--not` to cancel the first: `git rev-list --not --branches=* --not $newrev` – Mort Mar 02 '17 at 15:06
  • This answer is 95% correct. Just missing "--tags". I maintain a free Bitbucket plugin (Control Freak) that looks at all net-new commits for some of its checks. Here's what I do when "oldrev" is all-zeroes: git rev-list --not --branches --tags. I suspect it's extremely rare for tags to sit on unlabelled terminal commits (basically branch tips but without branch labels), but I like to be careful just in case. – G. Sylvie Davies Aug 28 '19 at 22:19
7

I just figured it out myself.

git log newref --not otherheads

is the key to get all logs of a branch that are not on any other branch. Below is my python script to check for the correct maximum line length of the commit messages.

import sys
import commands

ref = sys.argv[1]
old = sys.argv[2]
new = sys.argv[3]

x = 0

# only a tag is pushed to server, nothing to check
if ref.find('refs/tags/') >= 0:
  if len(ref.strip('refs/tags/')) > 25:
    print 'tag name is longer than 25 characters'
    exit(1)
  else:
    exit(0)
# either a new branch is pushed or an empty repo is being pushed
if old == '0000000000000000000000000000000000000000':
  heads = commands.getoutput("git for-each-ref --format='%(refname)' 'refs/heads/*'")
  heads = heads.replace(ref+'\n','').replace('\n',' ')
  hashes = commands.getoutput('git log '+new+' --pretty=%H --not '+heads).split('\n')
else:
  hashes = commands.getoutput('git log '+old+'..'+new+' --pretty=%H').split('\n')

for hash in hashes:
  subject = commands.getoutput('git show '+hash+' --format=%s --summary').split('\n')
  body = commands.getoutput('git show '+hash+' --format=%b --summary').split('\n')

  if len(subject[0]) > 75:
    print
    print 'commit: '+hash
    print 'bad commit message(s): header line is too long or second line is not blank (max 75 chars)'
    print 'bad line: "%s"' % subject[0]
    print 'length of header line: %d' % len(subject[0])
    print 'try again with correct message format'
    print
    x = 1

  for line in body: 
    if len(line) > 75:
      print
      print 'commit: '+hash
      print 'bad commit message(s): description lines are too long (max 75 chars)'
      print 'bad line: "%s"' % line
      print 'length of  line: %d' % len(line)
      print 'try again with correct message format'
      print
      x = 1

if x == 0:
  exit(0)
else:
  exit(1)
mattmilten
  • 6,242
  • 3
  • 35
  • 65
  • Using `refs/heads/*` in `for-each-ref` is not a good idea. It won't match any branches with slashes in the name (e.g., `foo/bar`). You should just use `refs/heads/` (no `*`). But, see Joseph's answer below for a simpler alternative. – MadScientist Jun 01 '14 at 19:47
2

I solved this for my update hook using the following:

if [ "$oldrev" -eq 0 ]; then
git log "$(git show-branch --merge-base)".."$newrev"; else
foo;
fi
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343