AnswerChaser AnswerChaser - 25 days ago 6x
Git Question

Detect when the remote tracked branch has been force updated in Git

How can I detect, from a script, if a remote tracked branch has been force updated in Git?

Can I know which commits differ between local and remote branch?



How can I detect, from a script, if a remote tracked branch has been force updated in Git?

It's obvious visually in git fetch output (which actually goes to stderr):

   d1a8e9c..0d12484  next       -> origin/next
 + 085c69e...636c0c5 pu         -> origin/pu  (forced update)
   5c9b3b6..9c14a42  todo       -> origin/todo

(this is from updating the Git repository for Git). In this case pu, the pickup branch, was force-updated, and both the + and (forced update) indicate so.

To detect it after the fact, use the reflog for the remote:

$ git reflog origin/pu
636c0c5 refs/remotes/origin/pu@{0}: fetch: forced-update
085c69e refs/remotes/origin/pu@{1}: fetch: forced-update
eb0e753 refs/remotes/origin/pu@{2}: fetch: forced-update
7d82ce0 refs/remotes/origin/pu@{3}: fetch: forced-update
091bd8f refs/remotes/origin/pu@{4}: fetch -a origin: forced-update
... [snip]
a6f7b76 refs/remotes/origin/pu@{32}: pull --rebase: forced-update
ca1441a refs/remotes/origin/pu@{45}: fetch: fast-forward
a753ca6 refs/remotes/origin/pu@{46}: 
14a019f refs/remotes/origin/pu@{47}: fetch: forced-update

(I have no idea what happened with origin/pu@{46} here, but I wanted to include @{45} to show that sometimes, even the pu branch gets fast-forwarded. :-) )

Note that reflogs for the remote must be enabled, for this to work. The lines may not always say fetch, as in the above example with pull --rebase.

Another way to detect that this was a forced update is to compare each reflog. A fast-forward occurs when the label moves from an ancestor to a descendant, while a forced-update occurs when the label moves from a one commit to another commit that is not a descendant of the previous one. Hence:

$ git merge-base --is-ancestor origin/pu@{46} origin/pu@{45} &&
> echo fast-forward || echo forced
$ git merge-base --is-ancestor origin/pu@{3} origin/pu@{2} &&
> echo fast-forward || echo forced

This shows that moving from @{46} to @{45} was a fast forward (as it says in the reflog), while moving from @{3} to @{2} was a forced update (as it also says in the reflog). Using git merge-base --is-ancestor like this is probably superior to checking literal strings in the reflog update messages, since it does not depend on fickle human-oriented output.

Can I know which commits differ between local and remote branch?

This seems unrelated to whether there was a forced update, but yes. Git has a special syntax using three (not two, but three) dots, which produces a symmetric difference.

Note: I am going to combine the symmetric difference with --left-right here for illustration purposes. For programmatic purposes, one usually either needs this, or --left-only or --right-only or some combination of such options with additional flags like --cherry-pick. This particular output from another repository is not very interesting as everything is on the "left side" (the master branch, not origin/master):

$ git rev-list --left-right master...origin/master

If there were some commit on origin/master that was not on master—for instance, if one of these two were on master and other on origin/master—the output would be more like:


The result of the symmetric difference operator is, I think, best illustrated in graphical terms. Suppose we draw part of the commit graph like this:

...--A--B--C--D     <-- branch
          E--F--G   <-- origin/branch

(This is the kind of graph that we see all the time when we're working on branch branch: We make two commits C and D, then run git fetch and find that we have just brought in three upstream commits, E, F, and G. Note that there was no forced update involved here.)

Using the three-dot notation, branch...origin/branch or origin/branch...branch, we tell Git to select all commits that are on either the left-side branch or the right-side branch, but that are not on both branches. Commit B, the merge base of the two branches, is on both branches, as is commit A and all earlier commits (all ancestors of commit B). So this gets us commits C, D, E, F, and G, and excludes B and all its ancestors.

Compare this with the two-dot notation, e.g., origin/branch..branch. Here we tell Git to select all commits that are on the right-hand side, but then to exclude all commits that are on the left-hand side. Commits B through D are on branch, which is the right hand side, as are ancestors like A and whatever might be before A. Commits A and B, and of course E, F, and G, and everything before A as well, are all on the left hand side (origin/branch). So this excludes G and F and E, which is a bit pointless since they were not included in the first place, but then also excludes B and A and all their ancestors. This is what gets us "commits that are only on branch".

When using set subtraction (two dots), the order of the two selectors (origin/branch..branch) is crucial: reversing it gets us commits that are only on origin/branch, instead of commits that are only on branch. When using the symmetric difference (three dots), the order is less important: we're getting all commits on either branch that are not on both branches. Swapping them around still gets us the same set of commits. The order only matters for decoration operators like --left-right, which mark which side of the symmetric difference brought the commits into the set.