Determine if a merge will resolve via fast-forward
Asked Answered
M

4

14

I want to know if a particular merge will resolve via "fast-forward" or not before running the merge command.

I know I can specifically request for the merge to not be resolved via "fast-forward" (using the --no-ff option). Or that I can try to resolve the merge only by fast-forward (using the --ff option).

But sometimes I want to know if a particular merge is going to resolve via fast-forward before I run it. I realise that I can in theory work it out by digging through the history tree. And I also realise that I could run the merge and see what happens, but this becomes problematic if I then decide I would prefer for the merge to be resolved in the other way, as I have to undo the merge (by repointing the branch tags in the ref-log) and do it again.

NOTE: The --dry-run question (Is there a git-merge --dry-run option?) is much more about looking at what merge conflicts may exist in a merge, and not about merges that may resolve via fast-forward.

Matrimonial answered 6/6, 2016 at 2:47 Comment(1)
Possible duplicate of Is there a git-merge --dry-run option?Arabic
C
14

Summary: git merge-base --is-ancestor tests whether one commit is an ancestor of another (where commits are considered to be their own ancestors, which is a particularly weird form of incest, perhaps :-) ). Since a branch label can only be fast-forwarded by git merge when the current branch (HEAD) points to a commit that is an ancestor of the other commit, we can use this to determine whether git merge could do a fast-forward operation.

It looks like you wanted this posted as an answer, so I have converted it to a working git alias, which you can put in your global git configuration. The alias is a bit long and complicated and it would probably be best to just cut-and-paste this into your git config alias section:

canff = "!f() { if [ $# -gt 0 ]; then b=\"$1\"; git rev-parse -q --verify \"$b^{commit}\" >/dev/null || { printf \"%s: not a valid commit specifier\n\" \"$b\"; return 1; } else b=$(git rev-parse --symbolic-full-name --abbrev-ref @{u}) || return $?; fi; if git merge-base --is-ancestor HEAD \"$b\"; then echo \"merge with $b can fast-forward\"; else echo \"merge with $b cannot fast-forward\"; fi; }; f"

Here is the same thing written as a shell script, in a more readable fashion, and some commentary:

#! /bin/sh
#
# canff - test whether it is possible to fast-forward to
# a given commit (which may be a branch name).  If given
# no arguments, find the upstream of the current (HEAD) branch.

# First, define a small function to print the upstream name
# of the current branch.  If no upstream is set, this prints a
# message to stderr and returns with failure (nonzero).
upstream_name() {
    git rev-parse --symbolic-full-name --abbrev-ref @{u}
}

# Now define a function to detect fast-forward-ability.
canff() {
    local b # branch name or commit ID

    if [ $# -gt 0 ]; then  # at least 1 argument given
        b="$1"
        # make sure it is or can be converted to a commit ID.
        git rev-parse -q --verify "$b^{commit}" >/dev/null || {
            printf "%s: not a valid commit specifier\n" "$b"
            return 1
        }
    else
        # no arguments: find upstream, or bail out
        b=$(upstream_name) || return $?
    fi
    # now test whether git merge --ff-only could succeed on $b
    if git merge-base --is-ancestor HEAD "$b"; then
         echo "merge with $b can fast-forward"
    else
         echo "merge with $b cannot fast-forward"
    fi
}

The shell script just needs a main section to drive it, which is the call to f after the alias. The alias itself simply shovels everything from canff and upstream_name into one line. Git's config file rules then require that the entire alias be quoted with double quotes, which in turn requires that all internal double quotes be converted to backslash-double-quote sequences.

(I also took out the local b statement, since as an alias, this fires up a new instance of the shell each time, so variable-name hygiene becomes unimportant.)

(It's actually possible to write the alias as multiple lines. Just prefix each newline with backslash. However, this alias is so complex that it looks ugly that way too, so I ended up just leaving it one big line.)

Charla answered 8/6, 2016 at 22:55 Comment(2)
is there any indicator after the merge is done ie you've done git merge master? Something that would easily tell you it was a fast-forward merge. Or is there any option that you can add to git merge master so that it would tell give you more metadata about the merge?Grabowski
@Honey: If git merge performs a fast-forward, there is no merge commit at all, so there is no place to store any metadata. If you want to store metadata, you need a merge commit, so you must disable fast-forward merging using git merge --no-ff. You then get a real merge, even though it's done trivially.Charla
H
2

You could test if git merge-base <branch1> <branch2> is equal to git rev-parse <branch1>. If equal, a ff merge or already-up-to-date when you run git merge <branch1> <branch2>. If not, a non-ff merge.

function isff(){
a=$(git merge-base $1 $2)
b=$(git rev-parse $1)
c=$(git rev-parse $2)
if [[ "$b"  == "$c" ]] || [[ "$a" == "$c" ]];then
    echo merge dry run: already up-to-date
    return
fi
if [ "$a" == "$b" ];then
    echo merge dry run: a fast forward merge
else
    echo merge dry run: a non fast forward merge
fi
}

isff master topic

Herrmann answered 6/6, 2016 at 3:25 Comment(2)
Simpler (though won't give you "already up to date" case, just "can fast-forward"): do one test, git merge-base --is-ancestor HEAD target && echo can fast-forward.Charla
I'm not really worried about the case of merges that are already up-to-date: this is already typically obvious to me. @Charla has the best solution to my problem. If you'll put that as a full answer, I'll accept it.Matrimonial
E
1

There's a similar, but not quite the same, question asked here: Is there a git-merge --dry-run option?

The first answer looks like it might be what you're looking for.

Specifically, a merge with the --no-commit flag and using --abort when you've seen enough and want to go back and do the actual merge.

Ebon answered 6/6, 2016 at 2:53 Comment(0)
M
0

Looking at the question referenced by @Ashwin-nair, I found what I believe is an answer.

For

git checkout MY_BRANCH 
git merge OTHER_BRANCH 

If the output of

git branch --contains MY_BRANCH

contains OTHER_BRANCH, then a merge with OTHER_BRANCH can be resolved via fast-forward.

Matrimonial answered 6/6, 2016 at 3:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.