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.
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
$ 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
$ 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
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:
- You can log the commits your feature branch is missing. This is useful to check how far your feature branch is behind master and gives you a chance to fix potential conflicts upfront.
$ git log feature..master
- After fetching changes from upstream, you can peek what’s come into the codebase before merging it.
$ 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.