miracle2k
miracle2k

Reputation: 32127

Strange git merge issue

This is how gitk currently looks in one of our projects:

https://dl.dropbox.com/u/2582508/gitk.png

This apparently, from all we can tell, happened after a single "git merge" was done with a remote branch - we're not sure why or what is going on. Any idea what happened here?

More importantly, what's the best way to fix it? Those merge commits are empty, but when doing "git rebase -i" merge commits generally don't seem to appear.

Most importantly, I would prefer to not make the history incompatible with other clones, i.e. they should still able to pull/push/merge with it. Is that even possible?

Upvotes: 4

Views: 1066

Answers (1)

Pat Notz
Pat Notz

Reputation: 214506

This is the result of someone updating their project using git pull (or, equivalently, git fetch and git merge). Imagine you're repository looks like this:

o---o---o [origin/master]
         \
          A---B---C [master]

That is, you've done commits A, B and C on top of what was in the original repo.

In the mean time, some else makes changes and pushes them up to the shared repository. If you then run git fetch your repository looks like this:

o---o---o---D---E---F [origin/master]
         \
          A---B---C [master]

Now, if you run git merge (remember: git pull is just git fetch followed by git merge). You'll have this:

o---o---o---D---E---F [origin/master]
         \           \
          A---B---C---G [master]

Assuming all goes well, G is probably just a "stupid" merge commit; technically, the state of G is different from F and C and so it must be considered differently.

Now, if you push this change, you'll have this:

o---o---o---D---E---F 
         \           \
          A---B---C---G [origin/master]

And if you continue development you'll get this:

o---o---o---D---E---F 
         \           \
          A---B---C---G [origin/master]
                       \
                        H---I---J [master]

Now, if you keep doing this (and if many people keep doing this) you'll end up with a tree like the one in your picture. This isn't "wrong" but many people don't like it because it makes the development history very hard to follow.

The solution to this problem is to teach people to rebase. Rebasing will (as you noted) remote the useless merge commits and git you a much cleaner history. In the case above, you'll end up with a linear development history. That's much easier to follow. However, you need to know that after you rebase you need to rebuild and retest your code... just because the code merged easily doesn't mean the result is correct.

If you're using a central repository to share an official development line, you can implement a pre-receive hook that detects these automatic ("stupid"/"useless") merge commits and rejects the push -- forcing the user to either rebase. Actually, you want the hook to look for the default merge commit message... so, if you really do want to keep the merge commit (sometimes that makes sense) you must at least write an intelligent commit message.

Here's an example hook. I stripped a bunch of extra stuff out so I didn't get to test it.

#!/bin/bash

# This script is based on Gnome's pre-receive-check-policy hook.
# This script *only* checks for extraneous merge commits.

# Used in some of the messages
server=git.wherever.com

GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)

in_import() {
    test -e "$GIT_DIR/pending"
}

forced() {
    test -n "$GNOME_GIT_FORCE"
}

check_commit() {
    commit=$1

    subject="$(git log $commit -1 --pretty=format:%s)"
    if expr "$subject" : ".*Merge branch.*of.*\(git\|ssh\):" > /dev/null 2>&1; then
          if ! in_import && ! forced ; then
                cat &2
---
The commit:

EOF
        git log $commit -1 >&2
        cat &2

Looks like it was produced by typing 'git pull' without the --rebase
option when you had local changes. Running 'git  pull --rebase' now
will fix the problem. Then please try, 'git push' again. Please see:

  http://live.gnome.org/Git/Help/ExtraMergeCommits
---
EOF
        exit 1
          fi
    fi
}

check_ref_update() {
    oldrev=$1
    newrev=$2
    refname=$3

    change_type=update
    if expr $oldrev : "^0\+$" > /dev/null 2>&1; then
    change_type=create
    fi

    if expr $newrev : "^0\+$" > /dev/null 2>&1; then
          if [ x$change_type = xcreate ] ; then
             # Deleting an invalid ref, allow
                return 0
          fi
          change_type=delete
    fi

    case $refname in
     refs/heads/*)
        # Branch update
        branchname=${refname#refs/heads/}

        range=
        # For new commits introduced with this branch update, we want to
        # run some checks to catch common mistakes.
        #
        # Expression here is same as in post-receive-notify-cia; we take 
        # all the branches in the repo, as "^/ref/heads/branchname", other
       # than the branch we are actualy committing to, and exclude commits
       # already on those branches from the list of commits between
       # $oldrev and $newrev.

        if [ -n "$range" ] ; then
        for merged in $(git rev-parse --symbolic-full-name --not --branches | \
                    egrep -v "^\^$refname$" | \
            git rev-list --reverse --stdin "$range"); do
            check_commit $merged
        done
        fi
        ;;
    esac

    return 0
}

if [ $# = 3 ] ; then
    check_ref_update $@
else
    while read oldrev newrev refname; do
    check_ref_update $oldrev $newrev $refname
    done
fi

exit 0

Upvotes: 3

Related Questions