Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

git: why can't I delete my branch after a squash merge?

I have a git repo with mainline (equivalent to master) and some local feature branches. For example:

$ git branch * mainline   feature1   feature2   feature3 

When I do the following, I am able to squash merge all of my edits in a feature branch into one commit to mainline:

$ git checkout mainline $ git pull $ git checkout feature1 $ git rebase mainline $ git checkout mainline $ git merge --squash feature1 $ git commit $ git push 

My question is, at this point, when I try to delete the feature1 branch, it tells me it is not fully merged:

$ git branch -d feature1 error: The branch 'feature1' is not fully merged. If you are sure you want to delete it, run 'git branch -D feature1'. 

What causes this error? I thought git merge --squash feature1 merged feature1 into mainline.

like image 993
user1002119 Avatar asked Jan 30 '17 22:01

user1002119


People also ask

Can you delete branches after merge?

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

What happens if you squash a merge commit?

Specifically, squashing throws away all our hard work in building a specific commit graph. Instead, squashing takes all the changes and squashes them together into a single commit. It's as if you made all the changes at one sitting and committed them as a unit.

Should you delete remote branches after merge?

When you're done with a branch and it has been merged into master, delete it. A new branch can be made off of the most recent commit on the master branch. Also, while it is ok to hang onto branches after you've merged them into the master they will begin to pile up.

How do I abort git merge squash?

git merge --abort is equivalent to git reset --merge when MERGE_HEAD is present. After a failed merge, when there is no MERGE_HEAD , the failed merge can be undone with git reset --merge but not necessarily with git merge --abort , so they are not only old and new syntax for the same thing.


2 Answers

This happens because Git doesn't know that the squash merge is "equivalent to" the various branch-specific commits. You must forcibly delete the branch, with git branch -D instead of git branch -d.

(The rest of this is merely about why this is the case.)

Draw the commit graph

Let's draw (part of) the commit graph (this step is appropriate for so many things in Git...). In fact, let's step back one more step, so that we start before your git rebase, with something like this:

...--o--o--o     <-- mainline       \        A--B--C   <-- feature1 

A branch name, like mainline or feature1, points only to one specific commit. That commit points back (leftward) to a previous commit, and so on, and it's these backward pointers that form the actual branches.

The top row of commits, all just called o here, are kind of boring, so we didn't give them letter-names. The bottom row of commits, A-B-C, are only on branch feature1. C is the newest such commit; it leads back to B, which leads back to A, which leads back to one of the boring o commits. (As an aside: the leftmost o commit, along with all earlier commits in the ... section, is on both branches.)

When you ran git rebase, the three A-B-C commits were copied to new commits appended to the tip of mainline, giving us:

...--o--o--o            <-- mainline       \     \        \     A'-B'-C'   <-- feature1         \          A--B--C       [old feature1, now abandoned] 

The new A'-B'-C' commits are mostly the same as the original three, but they are moved in the graph. (Note that all three boring o commits are on both branches now.) Abandoning the original three means that Git usually doesn't have to compare the copies to the originals. (If the originals had been reachable by some other name—a branch that appended to the old feature1, for instance—Git can figure this out, at least in most cases. The precise details of how Git figures this out are not particularly important here.)

Anyway, now you go on to run git checkout mainline; git merge --squash feature1. This makes one new commit that is a "squash copy" of the three—or however many—commits that are on feature1. I will stop drawing the old abandoned ones, and call the new squash-commit S for Squash:

...--o--o--o--S         <-- mainline             \              A'-B'-C'   <-- feature1 

"Delete safety" is determined entirely by commit history

When you ask Git to delete feature1, it performs a safety check: "is feature1 merged into mainline?" This "is merged into" test is based purely on the graph connectivity. The name mainline points to commit S; commit S points back to the first boring o commit, which leads back to more boring o commits. Commit C', the tip of feature1, is not reachable from S: we're not allowed to move rightward, only leftward.

Contrast this with making a "normal" merge M:

...--o--o--o---------M   <-- mainline             \       /              A'-B'-C'    <-- feature1 

Using the same test—"is the tip commit of feature1 reachable from the tip commit of mainline?"—the answer is now yes, because commit M has a down-and-left link to commit C'. (Commit C' is, in Git internal parlance, the second parent of merge commit M.)

Since squash merges are not actually merges, though, there is no connection from S back to C'.

Again, Git doesn't even try to see if S is "the same as" A', B', or C'. If it did, though, it would say "not the same", because S is only the same as the sum of all three commits. The only way to get S to match the squashed commits is to have only one such commit (and in this case, there's no need to squash in the first place).

like image 180
torek Avatar answered Oct 11 '22 13:10

torek


You can in fact determine and delete squash-merged branches, though it is non trivial.

git for-each-ref refs/heads/ "--format=%(refname:short)" | while read branch; do mergeBase=$(git merge-base master $branch) && [[ $(git cherry master $(git commit-tree $(git rev-parse $branch\^{tree}) -p $mergeBase -m _)) == "-"* ]] && git branch -D $branch; done 

To determine if a branch is squash-merged, git-delete-squashed creates a temporary dangling squashed commit with git commit-tree. Then it uses git cherry to check if the squashed commit has already been applied to master. If so, it deletes the branch.

All credits go to Teddy Katz, https://github.com/not-an-aardvark/git-delete-squashed

like image 30
RussKie Avatar answered Oct 11 '22 12:10

RussKie