1

Here was my workflow:

git checkout master
git pull --all --ff-only --prune
git checkout -b feature-branch
<modified a file>
git add file.py
git commit -m "modify file"
git push -u origin HEAD

Then, in the GitHub UI, I pressed squash and merge.

Then, in the terminal:

git checkout master
git pull --all --ff-only --prune

Now, if I do git diff master feature-branch, nothing shows up. But if I do git branch --merged, then only master appears.

How should I adjust my workflow so that git branch --merged will show feature-branch?

ignoring_gravity
  • 6,677
  • 4
  • 32
  • 65

1 Answers1

3

Then, in the GitHub UI, I pressed squash and merge.

This is the source of the problem. You have a choice: stop using that button, or get used to the fact that --merged literally cannot tell you about the result.

One of the more aggravating (to me anyway) features of GitHub is how it hides the commit graph from you, as if the commit graph were not important. But Git is nearly nothing but commits, and the commit graph is crucial. You're just somehow expected to know that the SQUASH AND MERGE button isn't going to do what you expect and want, here. Neither will REBASE AND MERGE. Only MERGE will cooperate.

Rather than write one of my long-form answers here, I'll just refer you to, e.g., this answer to a mostly-unrelated question. Having read that, consider what happens with a real merge, where we have two chains of commits that split up at some point:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

If we run, at the command line, git merge branch2—remember that we're on branch1 here—and if Git can do all of the merge on its own, we quickly get to this state:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

The new commit M, which is a merge commit, has both commits J and L as its (two) parents.1 Remember (as noted in that other answer) that Git works backwards, from the last commit as found by some branch name. That means that from commit M, Git will work backwards to both J and L.

When you use git branch --merged, Git looks for names2 that point to any of the commits reached by walking backwards from the commit you specify. That is, you say git branch --merged branch1 and Git looks at:

  • commit M first: there's a name that points here, branch1, so this name comes out;
  • commit J, either next or after L and/or K, but there's no name that points here;
  • commit L, soon, though perhaps after J and I: there's a name that points here, branch2, so this name comes out;
  • commit I: there's no name that points here;
  • commit K: there's no name for this commit either;
  • commit H: there might be a name for this commit, if both branches branch off some main-line commit for instance, but we didn't draw one;
  • commit G;

and so on, backwards, all the way to the very first commit.

Whatever names come up during this walk, git branch --merged gathers them up into a big list of names, and then sorts and prints out those names.


1While it does not matter for this particular situation, commit J is its first parent, and L is its second. This is useful later, with the --first-parent option to git log for instance.

2Note that git branch --merged checks only branch names, but git branch -a --merged checks both branch names like feature and remote-tracking names like origin/feature. In your case, you care about (your) branch names, so you don't need the -a here.


Those GitHub clicky buttons don't all merge

When you use SQUASH AND MERGE, GitHub does not make a merge commit. In fact, only the MERGE button makes a merge commit. The other two buttons work by copying commits.

Git is all about the commits, with names like branch1 and branch2 just serving to find commits. They hold the commits' actual big ugly hash IDs, so that we don't have to deal with them. But the linkage from one commit back to its parent—or for a merge commit, parents, plural—uses the big ugly hash ID. Those are the names that Git sees as it walks the commit graph, i.e., traverses the set of commits found by starting from some given commit and working backwards. At merge commits, which lead back into two paths,3 a graph-walk generally has to follow both paths.4

But suppose we start with the same starting graph:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

and, instead of making a merge commit, make a single new commit—let's call it S for squash—that does to J what the K-L combination of commits do to commit H? That is, we'll have Git figure out what changed from H to L, add the changes from H to J, and apply the combined changes to H. This is how Git would create merge commit M, so we get the same snapshot from this process. But instead of merge commit M, we have Git make squash commit S, that has only one parent. The result looks like this:5

          I--J--S   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

We can and should delete the name branch2 now. If we do, we'll never see commits K and L again.6 But git branch --merged will start at S and work backwards, visiting commits S, J, I, H, G, and so on. This never visits commit L, so git branch --merged won't print the name branch2.

The REBASE AND MERGE button is similar, except that instead of using the merge machinery—which makes the one S commit—it uses Git's rebase machinery to copy both commits K and L, resulting in:

          I--J--K'-L'  <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

(though see footnote 5 again here). We should now either delete the name branch2 or make the name branch2 point to commit L'. As before, though, we can't really see this on GitHub.


3Think about the fact that when you make branches, you create two paths that diverge. When you use git merge, you combine these into one path. But two paths that combine into one are one path that divides into two, if you're walking the opposite direction!

4The --first-parent flag I mentioned earlier tells Git not to bother with any but the first path. Merge commits can technically have three or more parent commits, though most practical merges just have two.

5I've skipped over several steps here. The graph looks like this in your repository after you git fetch the new commit from GitHub and update your own branch name branch1. As noted in that other answer, git fetch gets the new commit(s) from GitHub and introduces them into your own copy of the repository. This renames their branches to become your remote-tracking names, so that you have an origin/branch1 pointing to the new squash commit S.

6There are ways to find them again, but they involve using Git's reflogs. When you delete a branch, this deletes the branch's reflog as well. Some people consider this a mistake or bug in Git: if Git kept the reflog, you could un-delete the branch. A future version of Git might keep the reflogs of deleted branches, at least for the standard reflog expiration. In any case, the HEAD reflog may contain the hash IDs of the commits, which will preserve them for some time and allow you to find them. This is useful in emergencies, but recovering commits this way is rarely much fun.


The only way to see all this is in your own repository

Because GitHub hide the actual merge graph, and nobody pays any attention to the commit hash IDs,7 when you use GitHub to see what's happening, you just can't actually see it. You must run git fetch to get the commits into your own repository, then use some kind of graph-viewer to observe the results. See Pretty git branch graphs for many options here. My personal go-to is Git Log With A Dog, which is named per this answer from 2016.8


7This suggests that people probably should pay very close attention to those hash IDs. But that's an attempt to change human nature, and I don't think it's a good plan.

8I actually have an alias for this from much longer ago, git lola from 2010. At least one person I know prefers git adog, and I have been tempted to switch, but they're already in my typing muscle-memory at this point.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Awesome answer, thanks! I can't get away from "squash and merge" as that's the team's preference, I'll just have to remember to manually delete branches when they're merged – ignoring_gravity Oct 09 '20 at 08:49
  • 1
    For what it's worth, I had to deal with a different, but similar, situation, and I wrote a script to clean up but had some bugs in the script. I didn't actually use it enough to fix the bugs—manual "remember to clean up" just worked better, in the end. :-) – torek Oct 09 '20 at 09:22