Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Determine if a merge will resolve via fast-forward

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.

like image 740
uayebforever Avatar asked Jun 06 '16 02:06

uayebforever


2 Answers

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.)

like image 86
torek Avatar answered Oct 19 '22 07:10

torek


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

like image 32
ElpieKay Avatar answered Oct 19 '22 07:10

ElpieKay