Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deleting merged branch gives "error: The branch X is not fully merged...'

Tags:

git

merge

github

I have been caught in a pickle. And am unable to find my answers from other SO questions or from reading the git documentation. I would really appreciate your help here.

After merging my branch on Github, I delete my branch from the UI (remote). Periodically I prune my local branches:

git checkout master
git pull origin master
git fetch -p
git branch -d X
Error: error: The branch X is not fully merged. If you are sure you want to delete it, run 'git branch -D X'

The branch X has been merged. And I can see the commits from X when I do git log on master. Why does this happen? I want to use -d and not -D since this could lead to disastrously deletion of branches that were genuinely not merged.

like image 547
Struggler Avatar asked Nov 09 '16 20:11

Struggler


People also ask

Why the branch is not fully merged?

There are times when you get an “not fully merged” error for a git branch, usually when trying to delete a local branch from your machine. It's a safe bet that something in your local branch has not actually made it to the remote repository and we should do some investigating.

What happens if you delete a merged branch?

There's no problem in deleting branches that have been merged in. All the commits are still available in the history, and even in the GitHub interface, they will still show up (see, e.g., this PR which refers to a fork that I've deleted after the PR got accepted).

How do I force delete a branch?

Deleting a branch LOCALLY Delete a branch with git branch -d <branch> . The -d option will delete the branch only if it has already been pushed and merged with the remote branch. Use -D instead if you want to force the branch to be deleted, even if it hasn't been pushed or merged yet.


2 Answers

There are several different tricky bits here.

The key question for git branch -d (delete without forcing1) is not "is the branch merged?", because that question is literally un-answerable. The question is instead "is the branch merged into <fill in something here>?"

This test was changed in Git version 1.7 (which means everyone should have it today, but check your Git version), in commit 99c419c91554e9f60940228006b7d39d42704da7 by Junio C Hamano:

branch -d: base the "already-merged" safety on the branch it merges with

When a branch is marked to merge with another ref (e.g. local 'next' that merges from and pushes back to origin's 'next', with 'branch.next.merge' set to 'refs/heads/next'), it makes little sense to base the "branch -d" safety, whose purpose is not to lose commits that are not merged to other branches, on the current branch. It is much more sensible to check if it is merged with the other branch it merges with.

Hence, there are two—or sometimes three, if you look at the above commit—questions that you (or Git) must answer to see whether -d is permitted:

  1. What is the upstream name for the branch we propose to delete? (If none, see below.)
  2. The branch name points to one specific commit (let's call this T for Tip). The upstream branch name also points to one specific commit (call this commit U).

    Is T an ancestor of U?

If the answer to question 2 is yes (i.e., T ≤ U), the deletion is permitted.

What if there is no upstream? Well, that's where "changed in 1.7" comes in: the original test was not T ≤ U but rather T ≤ HEAD. That test is still in there. Right now, if there is no upstream, the HEAD test is used instead of the upstream test. Meanwhile, for a "transition period" while everyone adjusts to the newfangled Git 1.7 behavior, you may also get a warning, or extra explanation, when there is an upstream. This warning persists even today, in Git 2.10 (almost 7 years later now: this is a very long transition period!):

if ((head_rev != reference_rev) &&
    in_merge_bases(rev, head_rev) != merged) {
        if (merged)
            warning("deleting branch ... not yet merged to HEAD.");
        else
            warning("not deleting branch ... even though it is merged to HEAD.");
}

(I trimmed some of the code for display purposes here): basically, if HEAD resolves to some other commit than the commit we used in the T ≤ test, and we'd get a different result for T ≤ HEAD than we got for T ≤ U, we add the extra warning message. (Note that the first part of the test is redundant: if we compared to HEAD because of the pre-1.7 compatibility and a missing upstream, then we'll get the same result if we compare to HEAD again. All we really need is the in_merge_bases test.)

How do you know what the upstream is? Well, there's actually an easy command line way to find out:

$ git rev-parse --abbrev-ref master@{u}
origin/master
$ git rev-parse --symbolic-full-name master@{u}
refs/remotes/origin/master

The --abbrev-ref variant gives you Git's typical abbreviated version, while --symbolic-full-name gives you the full reference name. Both fail if run on a branch with no upstream set. Of course, you can also use git branch -vv to see the abbreviated upstreams (for all branches).

How do you test this whole T ≤ U thing? The git merge-base --is-ancestor command does that for shell scripts, so:

$ git merge-base --is-ancestor master origin/master && echo yes || echo no

will tell you if master is an ancestor of origin/master (and for this purpose, "points to the same commit" counts as "is an ancestor", i.e., this is that very same ≤ test).

Whenever this transition period finally ends, the "use HEAD" test might go away entirely, or might continue be used for branches that have no upstream set. In any case, though, the question is always "is the to-be-deleted branch merged into ____? (fill in the blank)" and you must look at what fills the blank.

The commit(s) on the upstream that correspond to the commits on the branch you are proposing to delete must be (or at least have, in their history) the same commit, by commit hash ID, for that "is merged into" test to succeed. If feature/X was merged into origin/develop via a so-called "squash merge" (which is not a merge, though it's done by merging2), that commit ID won't match, and the "is merged into" test will always fail.


1Incidentally, as of Git 2.3, you can now add --force to git branch -d, instead of being required to use git branch -D, although of course -D still works.

2The distinction here is between "a merge"—merge as a noun, which means "a commit that is a merge commit" (which uses merge as an adjective), and "to merge"—merge as a verb, meaning the action of combining some sets of changes. The git merge command can:

  • do a fast forward, which neither merges-as-a-verb nor makes a merge-as-a-noun, so that there is no merge at all; or
  • do a real merge, which merges (verb) some changes to produce a merge (noun); or
  • do a "squash merge", which merges (verb) but produces a non-merge (which, for some inexplicable reason, you must manually commit).

A "squash merge" happens only if you ask for it, while a "fast-forward non-merge" happens only if it can and you don't prohibit it.

like image 137
torek Avatar answered Sep 21 '22 10:09

torek


This happens when the content of the commits differ enough from the branch you are attempting to delete. This could be because of a squash commit or a branch that is attached to a previous base then when they were merged by someone else on the remote.

If you feel confident that the changes were in fact merged (as you said) then a branch -D is safe.

Lastly,

disastrously deletion of branches

Is untrue. Deleting a branch is not an undo-able issue. All actions are stored in the git reflog and the commits that a branch pointed to stay for at least 30 days. If truly paranoid you could rename the branch instead. But in practice it is easier to just delete the branch.

In other words a branch s just a pointer. Deleting the pointer does not delete the content. And so you can easily resurrect the branch if you needed.

like image 22
Sukima Avatar answered Sep 21 '22 10:09

Sukima