views:

194

answers:

1

What I want to do:

I want to disallow any merge commits to be pushed to the central repository. The only exception being if the merge is between branches that exist in the central repository. I want to enforce this at the central repository.

Explanation of why I want to do this:

Note: If this explanation throws you off the trail of what I want to do, then ignore the explanation. Of course I'm happy to hear other ways to solve the issue I explain below, but the answer I'm interested in is to what I want to do as stated above.

I have a central git repository with a branch that several developers track. Each developer has a remote configured for that central repository's branch.

We follow a synchronous commit policy for this project, so each developer must always rebase their latest work on top of the remote branch HEAD before pushing. I would like to enforce this policy by disallowing any merge commits to ever be pushed to the central repository. The only exception being if the merge is between branches that exist in the central repository.

To simplify, I do not want developer's local tracking branches to ever be merged with the remote branch. But rather always be rebased on the remote branch.

We've partly enforced this on the developer's machine by setting branch.NAME.rebase = true which helps avoiding problems if the developer uses git pull, however we need a solution to enforce this on the central repository side.

A very base solution would be to refuse commits with the comment: "Merge branch 'NAME' of GITURL", however, something more along the lines of checking if all parents of a commit exist in the central repository's branch paths would be more interesting.

Suggestions? Solutions?

Edit:

This is what I have so far:

#!/bin/sh
read sha1old sha1new refname

# check if this is merge commit
merge_commit="`git rev-list --parents --merges --no-walk $sha1new 2> /dev/null`"
if test -n "$merge_commit"
then
  # this was a merge commit
  # $merge_commit contains: sha1new sha1parent_1 ... sha1parent_n
fi
exit 0

The place it gets troublesome is determining whether the ancestry of any two parents originate from a single branch. Also, because pre-receive hook is called before any refs are updated, if a push contains commits for two branches that exist in the remote, including a merge between those two branches, then I have no idea what the solution would be here...

+2  A: 

One way of preventing pushes that would create non-linear history is to set up a pre-receive hook which uses git rev-list --parents <OLD>..<NEW> to check for any new commit that has more than one parent, and exit in error if so. To deal with the second requirement, you could instead check that if there's more than one parent, those commits must all be on existing branches in the repository. I haven't tested this very much, but this pre-receive hook (or some variant of it) may be what you want:

#!/usr/bin/ruby -w

# A pre-receive hook that should refuse any pushes that would update
# master in such a way that a non-linear history would be created,
# except where it involves a merge from another branch in this
# repository.  This has only had very cursory testing.

# This is a suggested answer to:
#   http://stackoverflow.com/questions/2039773/have-remote-git-repository-refuse-local-branch-merge-commits-on-push

ref_to_check = "refs/heads/master"

rev_old, rev_new, ref = STDIN.read.split(" ")

if ref == ref_to_check
  merge_bases = `git merge-base #{rev_old} #{rev_new}`.strip.split(/\s+/)
  unless $?.success? and merge_bases.length == 1
    STDERR.puts "No unique merge base found between #{rev_old} and #{rev_new}"
    exit(1)
  end
  rev_list_output = `git rev-list --parents #{merge_bases[0]}..#{rev_new}`
  list_of_revs_with_parents = rev_list_output.strip.split(/[\r\n]+/)
  list_of_revs_with_parents.each do |line|
    rev_with_parents = line.strip.split(/\s+/)
    if rev_with_parents.length > 2      
      parents = rev_with_parents.slice(1,rev_with_parents.length)
      # The question says to permit non-linear history if the merge is
      # from another branch in the central repository, so check
      # whether that's the case.  (If you just want to prevent all
      # pushes that add non-linear history, just exit with error
      # here.)
      any_parent_not_on_any_branch = false
      parents.each do |p|
        branches = `git branch --contains #{p} 2> /dev/null`
        if $?.success? and ! branches.strip.empty?
          STDERR.puts "More than one parent of commit #{rev_with_parents[0]}"
          STDERR.puts "... but parent #{p} is on branches:"
          STDERR.puts branches
        else
          STDERR.puts "Parent #{p} not found on any other"
          STDERR.puts "branch in this repository"
          any_parent_not_on_any_branch = true
          break
        end
      end
      if any_parent_not_on_any_branch
        STDERR.puts "Refusing push, since it would create non-linear history"
        STDERR.puts "for #{ref} and the merges don't just involve commits on"
        STDERR.puts "other branches in this repository."
        exit(2)
      end
    end
  end
end

I hope that's of some use.

Mark Longair
thanks! I certainly wasn't expecting a fully written implementation.It certainly looks like what I want, I'll run it through some tests.
chris
First, I'm having to port this as I don't have ruby installed.However, as I understand this script, it will still allow non linear history in the case where the commit consists of a merge point between to diverging commit paths on a single branch. Which is typically what you'll see from non-rebased local tracking branches. I haven't verified this through testing yet, so I might be missing something in my reading of the script.
chris
If a user is working on master, merging from origin/master and origin/master is linear, I don't think that can happen. If those conditions are met, any merge between that user's master and their origin/master will be a linear fast-forward except in one of the cases where master is not present in origin/master. If the user then tries to push such a merge, the hook will see that one of the parents is not already in master and refuse it. (Although if they wanted to be difficult they could have pushed their master to a new branch on the server so the parent is known there.)
Mark Longair
So I think it works, but reasoning about this isn't all that simple - if it doesn't break your workflow, it might be easier to say "all history must be linear" and then make sure the the other branches in the main repository are rebased before merging into master. Then you could leave out the "git branch --contains" check and just exit whenever there are two or more parents - it's more obvious that it's correct then, at least :)
Mark Longair