I'm developing a simple script to show local branches (their status, etc) and I'd like to know which of my branches have been already merged to the master branch. Normally I have several branches (corresponding to fixes/features, etc). Some of them have an associated PR that at some point get merged to the remote origin. So I'd like to know which ones have been merged (so I can delete them locally for i.e). The problem is that when I have new/fresh branches (with 0 commits on them) the following git command lists them as merged which is confusing:
git branch --merged master
In the following example (took from a candidate answer):
K--L <-- br1
/ \
...--F--G--H---M <-- master, br3
\
I--J <-- br2
I would like to show br1 as merged, but not br3 (br2 is not merged in this case obviously). Please, keep in mind that master is evolving all the time so the above diagram is not realistic, normally master will move quickly and br3 becomes behind master so br1. The difference between br1 and br3 is that br1 at some point diverged (due to commits) and then it was merged again to master. However, br3 is just pointing to a rev on master but has never had (so far) any different commits.
CodePudding user response:
No branch, in Git, ever has no commits on it. This is because a branch name is an entity holding the hash ID of a valid existing commit. That valid existing commit is a commit, so the branch has at least one commit on it:
...--F--G--H <-- master, br1, br2
Here the names master
, br1
, and br2
all select commit H
. The set of commits that is on each branch is exactly the same as the set of commits that is on the other two branches.
People often say that "br1
has no commits" when what they mean is "br1
has no commits that are not also on master
". What you can do is count the number of commits that are different between any two branch names:
K--L <-- br1
/ \
...--F--G--H---M <-- master, br3
\
I--J <-- br2
Here, the result of:
git rev-list --count --left-right master...br3
will be 0 0
because the two branches are even with each other, while the result of:
git rev-list --count --left-right br1...master
will be 0 1
because br1
is strictly behind master
(has no commits that master
lacks) but master
is strictly ahead of br1
(has commit M
, which br1
lacks). The result of:
git rev-list --count --left-right br2...master
will be 2 5
, because br2
has two commits (I-J
) that are not reachable from M
, while master
has five commits (G-H-M
and K-L-M
but Git avoids double-counting M
) that are not reachable from J
. (Commits F
and earlier are reachable from both and are therefore not counted.)
Note that this syntax requires the three dots between the two names, and the --left-right
option to split out the two counts. You can also use git branch --merged master
to quickly eliminate br2
as a candidate: the result of git branch --merged master
will be the list of names that are behind or even with master
. Having found that list, you can then eliminate any name that is even with master
:
if [ $(git rev-parse $branch) = $(git rev-parse master) ]; then
# branch is even with `master` so
# `git rev-list --left-right --count` would
# produce 0 0
else
# branch is behind `master`
fi
Note: do not use git branch
in a script; use git for-each-ref
instead. (Use git branch
if and only if you have a sufficiently ancient Git that git for-each-ref
lacks the --merged
option. However, if you have a Git that old, you should consider upgrading it.)
CodePudding user response:
tl;dr: Since your goal is to know which local branches you are done working on and can be deleted, the command you tried isn't sufficient. You'll need to do something else first, or use a different method entirely.
Explanation:
I think the source of your question really comes from the wording used by the branch
option --merged
, which is confusing since "merged" means something slightly different in Git than it does in English.
The documentation for --merged
states (emphasis mine):
With --merged, only branches merged into the named commit (i.e. the branches whose tip commits are reachable from the named commit) will be listed.
For example, after you merge my-branch
into master
, my-branch
is going to appear as "merged" when you run the command git branch --merged master
, but it doesn't appear in the list because you merged it in, it appears because after you merged it in the branch is now reachable from master
. Any new branch you create from master
without adding new commits is of course also reachable, and consequently will also be considered "merged" by Git. Note, after merging my-branch
into master
, if you add a new commit to my-branch
, it will no longer appear as "merged" from Git's POV even though you previously did "merge" it. The point here is whether or not you actually ran git merge
somewhere to merge a branch in, isn't necessarily relevant to whether or not Git considers the branch "reachable".
Possible Alternatives:
Simple: There is a very simple solution that would enable you to keep doing what you're doing. Any time you create a new branch, add an empty commit to it, perhaps with some comment that may be useful. For example:
git fetch
# make your branch...
# Add an empty commit:
git commit --allow-empty -m "wip: Improve performance by refactoring"
By adding a blank commit it will no longer be reachable from master
. When you're ready to work on that branch just amend (and remove the "wip: " prefix in the subject), or reset back a commit before you make your first commit on that branch.
Better? If you use a SCM tool to merge your branches into master
rather than doing it locally, and if your workflow is to delete your remote branches after they are merged in (which really ought to be the default and most tools offer this as a selectable option when completing a PR), then you can (and probably should) prune your remote tracking branches, and then you can identify which local branches used to have a remote tracking branch but no longer do. Here's an example of this.
Simplest? What if you just delete all "merged" branches, and use some other mechanism to determine what you have to work on (e.g. issue tracker or project management tool). Having a bunch of branch names sitting there with no new commits on them isn't necessarily helping you, other than to remind you that you need to do it.
Creative with a Caveat: Of all the solutions, I think this one is probably what you are envisioning in your head, but it has a pretty specific caveat in that it will only work if every one of your feature branch merges uses --no-ff
and creates a merge commit. So, if you always use --no-ff
(or if master
moves so quickly that you'll always need a merge commit and you don't rebase your feature branch), then you should be able to filter the branches by those who's tip commit is reachable by master
, and also if a merge commit exists on master
with that tip commit as one of it's parents. This might take a minute to script out but once you have it, you can re-use it indefinitely.