/posts /about

Git commit ranges

September 29, 2018.

Recently, a certain feature had to be removed from the codebase. Related commits of the feature were already merged to origin/master. I could have deleted commits and pushed then back to master but this would be dangerous. As a rule of thumb, public commits should never be removed, or in other words, public commit history should never be rewritten. This is one of the reasons why origin/master should be a protected branch.

There is a dedicated command serving purpose of undoing effects of commits and that is the revert command. It just reverts given commits as its name suggests by making additional commit.

Given one or more existing commits, revert the changes that the related patches introduce, and record some new commits that record them. This requires your working tree to be clean (no modifications from the HEAD commit).

Basic usage is just as shown below. It expects a varargs of commits and automatically makes a revert commit.

SYNOPSIS

git revert [--[no-]edit] [-n] [-m parent-number] [-s] [-S[<keyid>]] <commit>...

The problem here was that the feature was implemented in multiple commits. I could log those commits and pass each to the revert one by one, but that would be just drudgery work. There should be an easier way to accomplish that.

And here comes commit ranges to the rescue. A commit range refers to a set of consecutive commits in the commit history. It is expressed via double-dot syntax. Given that c and d are both commit identifiers in the same branch, c..d refers to a range starting from c until d (not included).

Now with this background, I need to be able to revert all commits of the feature. For illustration purposes, let’s say feature is implemented in n number of commits and the last (most recent) commit is denoted with c. I can refer to the first commit by c~(n-1). A range of c~n..c hereby returns all commits of the feature - note that end commit in the range is excluded.

Now that I can easily refer to commits of the feature, I can pass the range to revert command.

$ git revert c~n..c

This would revert each commit inside the range by making a new commit for each one that is being reverted. That makes extra n commits to do the job. I would rather prefer to do that in a single commit. This can be achieved with no-commit flag.

$ git revert --no-commit c~5..c
$ git commit

This flag applies the changes necessary to revert the named commits to your working tree and the index, but does not make the commits.

How is c..d resolved actually? What does it return?

It basically returns commits that is reachable by d but not by c.

It helps to think of it as Venn diagrams. Consider now each of c and d identifiers as a set of commits comprised of its ancestors and itself, then c..d range can be illustrated as

D (left) \ C (right)  source: wikipedia.org

It’s also possible for a range to return an empty set of commits. For example, if d is a less recent commit than c in the history and d is reachable by c, then there is no commit that d has but c doesn’t, i.e. d is a subset of c hence c..d would return an empty set.

The order of commits in the range also matter. c..d and d..c may well return totally different commit sets.

Some other practical cases


To point out a couple of more cases that involve commit ranges during my routine:

$ git log feature..master
$ git fetch origin master
$ git diff master..origin/master

I want to hear about how you use commit ranges so let me know of your use cases that I am not aware of.

Send feedback to me @karakays_.

← We've got the jazz  Binary encodings →