Git 201: Safely Using Rebase

This post is part of a Git 201 series on keeping your commit history clean.  The series assumes some prior knowledge of Git, so you may want to start here if you’re new to git.

✧✧✧

The rebase tool in git is extremely powerful, and therefore also rather dangerous. In fact, I’ve known engineers (usually those new to git) who won’t touch it at all. I hope this will convince you that you really can use it safely, and it’s actually very beneficial.

What is rebase?

To start, I’ll very briefly touch on what a rebase actually is. So, very briefly then, a rebase is a way to rewrite the history of a branch. Let’s assume you’re starting out with a standard working branch from master:

At this point, let’s say you want to update your branch to contain the new commits on master (i.e., D and F), but you don’t want to create a merge commit in the middle of your work. You could rebase instead:

git rebase master

This command will rewrite your current branch as though it had originally been created starting from F (the tip of master). In order to that, though, it will need to re-create each of the commits on your branch (C, E, and G). Remember that a commit is the difference applied to some prior commit. In our example, C is the changes applied to B. E contains changes applied to B, and G contains changes applied to E. Rebasing will be that we need to change things around so that C actually has F as a parent instead of B.

The problem is that git can’t just change C’s parent because there’s no guarantee that the changes represented by C will result in the same codebase when applied to F instead of B. It might be that you’d wind up with some completely different code if you did that. So, git needs to figure out what result C creates, and then figure out what changes to apply to F in order to create the same result. That will yield a completely new commit which we’ll call CC. Since E was based upon C, which has been replaced, git will need to create a new commit using the same process, which we’ll call EE. And, since E has been removed, that means we’ll need to replace G with GG. Once all of the commits have been created, git moves the branch pointer to the end of the newest commit:

While all this seems complicated, it’s all hidden inside of git, and you don’t really have to deal with any of it.  In the end, using rebase instead of merge just means changing a single command, and your commit history is simpler because it appears as though you created your branch from the right place all along.  If you’d like a much fuller tutorial with loads of depth, I’d recommend you head over here.

Rebasing across repos

If you’re working on a branch which only exists locally, then rebasing is pretty straight-forward to work with. It’s really when you’re working across multiple clones of a repo (e.g., your local clone, and the one up on GitHub) that things become a little more complicated.

Let’s say you’ve been working on a branch for a while, and somewhere along the way, you pushed the branch back to the origin (e.g., GitHub). Later on, though, you decide you want to rebase to pick up some changes from master. That leaves you in the state we see in this diagram:

If you were to pull right now, git would freak out just a bit. Your local version of the branch seems to have three new commits on it (CC, EE, and GG) while it’s missing three others (C, E, and G). Then, when git checks for merge conflicts, there’s all sorts of things which seem to conflict (C conflicts with CC, E conflicts with EE, etc.). It’s a complete mess.

So, the normal thing to do here is to force git to push your local version of the branch back to origin:

git push -f

This is telling git to disregard any weirdness between the local version of the branch and origin’s version of the branch, and just make origin look like your local copy. If you’re the only one making changes to the branch, this works just fine. The origin gets your new branch, and you can move right along. But… what if you aren’t the only one making changes?

Where rebasing goes wrong

Imagine if someone else noticed your branch, and decided to help you out by fixing a bug. They clone the repository, checkout your branch, add a commit, and push. Now the origin has all your changes as well as the bug fix from your friend. Except, in the meantime, you decided to rebase. That would mean you’re in a situation like this:

Now you’re stuck. If you pull in order to get commit H, you’re going to have all sorts of nasty conflicts. However, if you force push your branch back to origin (to avoid the conflicts), you’re going to lose commit H since you’re telling git to disregard the version of the branch on origin. And, if your friend neglected to tell you about the bug fix, you might do exactly that and never even realize.

Solution 1: Communicate

The best way to fix the problem is to avoid it in the first place. Communicate clearly with your teammates that this branch is a working branch, and that they shouldn’t push commits onto it. It’s a good idea for teams to adopt some clear conventions around this to make this kind of mistake hard to make (e.g., any branch stating with a username should only be changed by that user, branches with “shared”, “team” or some other prefix are expected to have multiple contributors).

If you can’t be sure you’re only one working on a branch, the next best thing is, before starting the rebase, talk with anyone who might be working with the branch. Say that you’re going to rebase it, and what they should expect. If anyone speaks up that they’re working on changes to that branch, then you know to hold off.

Once everyone has pushed up any outstanding changes, pull down the latest version of the branch, rebase, and then push everything back up as soon as possible. That looks like this:

git checkout mybranch
git pull
git rebase master
git push -f

Once you’ve finished, you’ll want to tell the other people working on the branch that they need to get the fresh version of the branch for themselves. That looks like:

git checkout master
git branch -D mybranch
git checkout mybranch

Solution 2: Restart the rebase

If you find yourself having just rebased and only then learn there are upstream changes you’re missing, the simplest way out of this difficulty is to simply ditch your rebase. Go back, and pull down the changes from the origin, and start over (after referring to solution 1). That would look something like this:

git checkout master
git branch -D mybranch
git checkout mybranch
git rebase master
git push -f

This will switch back to master (1), so that you can delete your local copy of the branch (2), and then grab the most recent version from the origin (3). Now, you can re-apply your rebase (4), and then push up the rebased branch before anyone else has a chance to mess things up again (5).

Solution 3: Start a new branch

If you find that you want to rebase right away, and don’t want to wait to coordinate with others who might be sharing your branch, a good plan is to isolate yourself from the potentially shared branch first, and then do your rebase.

git checkout mybranch
git checkout -b mybranch-2

At this point, you’ve got a brand new branch which only exists on your local machine, so no one else could possibly have done anything to it. That means you can go ahead and rebase all you like. When you push the branch back up to origin (e.g., GitHub), it will be the first time that particular branch has been pushed.

Of course, if someone else has added a commit to the old branch, it will still be stuck over there, and not on your new branch. If you want to to get their commit on your new branch, use git’s cherry pick feature:

git cherry-pick <hash>

This will create a new commit on your branch which will have the exact same effect on your branch as it did on the old one. Once you’ve rescued any errant commits, you can delete the old branch and continue from the new one.

✧✧✧

I’m hope this makes rebasing less scary, and helps you get a sense of when you’d use it and when not. And, of course, should things go wrong, I hope this gives you a good sense of how to recover.

Two last bits of advice… First, before rebasing, create a new branch from the head of the branch you’re going to rebase. That way, should things go completely wrong, you can just delete the rebased branch, and use your backup. And, finally, if you’re in the middle of a rebase which seems to be going a little nuts, you can always bail out by using:

git rebase --abort

So, feel free to experiment!

The Importance of Context

When studying history, the first rule of intellectual honesty is to never drop the context of the time period being studied. We stand at the end of a long line of people who screwed things up, figured out what went wrong, and came up with a better solution. We are the inheritors of thousands of years learning in every area of human endeavor: including morality. When studying history, any time you indignantly ask the question “How could they?”, it is imperative to stop yourself and ask the question again with curiosity instead. Really… how did it come to pass that people in a prior age thought it right and natural to act in ways we find foreign or even immoral now?

We can (and should) look back with our modern eyes and pass judgement on the moral systems people have used in the past. Most moral codes for most of history were atrocious by our modern moral understanding. However, when judging individual members of those societies, we must not lose our perspective and judge them by standards they never even knew existed. One can only judge a person from a prior historical period by asking whether they faithfully adhered to the best moral code they knew about and/or whether they helped to advance our understanding of morality as such.

This does mean that certain historical figures, though perhaps despicable when judged by our modern standards, where moral and virtuous in their own time. It is important that we judge the moral system, not the person who could have known no better.

Considering Women in History

When thinking of the treatment of women through history (just to pick one minority), we must apply the same respect for context we would for any other historical study. We can (and should) judge historical societies’ moral codes based upon their respect for women. However, we can only judge individual people for having better or worse views and actions compared to others who shared their context.

For example, a person who was skeptical of a woman’s right to vote in England of 1880 is hardly a villain when judged by the moral standards of that time. We now find that position repugnant, but not the person who holds it. Needless to day, a person in a modern context who held such a view would (rightfully) be considered morally bankrupt. Conversely, a person who was enlightened enough, in that place and time, to support women’s suffrage wasn’t merely a normal, decent person (as they would be today), but a one of unusual foresight and virtue.

Notice that I very deliberately used the word “person” throughout that example. We must remember that the suffragettes were themselves usually foresighted and virtuous even among the women of their day. Many women of the time were as skeptical of such things as “votes for women” as their spouses. They too were not villains, but people of ordinary character and understanding: for their own time.

But what about…

The really interesting question is what other moral issues were, at one point, perfectly acceptable, but are not any longer? For example, homophobia was once not only perfectly acceptable, but actively encouraged and legally enforced. However, in the United States today, LGBT+ people are legally protected (in many jurisdictions) and homophobia (in most communities) is actively regarded as backward and immoral. When did that moral stance shift? How did it happen? At what point do you consider someone slow to make the shift as being immoral?

Thoughts on Toxic Masculinity

I recently saw the Gillette commercial about toxic masculinity, and it’s gotten me thinking, especially when viewed along side the Egard Watches response video. I highly recommend you go watch both of them before continuing to read here.

The perspectives in both are reflected by the polarized responses I’ve been seeing since the Me Too movement picked up steam. Any time I see such extreme reactions to the same thing (the commercial, especially) among people who normally agree about many things, it makes me stop to ponder what’s going on.

Personally, I find it very easy to have enormous sympathy with the Me Too movement.  It is sadly all too easy to find many, many examples of women being treated unjustly in every era, and in every civilization which has ever existed.  Indeed, “unjust” hardly begins to describe centuries of disregard, disenfranchisement, oppression, torment, slavery, mutilation, rape, and murder which women have suffered across the span of human history. Given that the perpetrators have been overwhelmingly male, it’s all too easy to take a dim view of masculinity in general.

However, it is also true that many brilliant, talented, moral, and courageous men have moved our species forward in leaps and bounds. Many of these men were the ones who fought against oppressors of every sort (both literally and figuratively). Indeed, many of them fought, specifically, to oppose the tremendous injustice met out to women by other men of their time. Taking either the view that all men are monsters or that all men are innocent is too simplistic.

I view “toxic masculinity” as being what the philosopher, Ayn Rand, called a package deal. That is, a bunch of concepts grouped together with the effect (usually deliberate) of damning the good by linking it with the evil. In this case, the “package” contains a lot of elements which are, in fact, attitudes, beliefs, and cultural norms which each have been held by individual men. However, not all men exhibit all these traits, and, in fact, it’s very common for the negative traits to be concentrated in certain individuals, and positive ones in others.

But let’s get specific here. When I think of traits considered typically “masculine”, I get something like this:

  • physical traits (size, strength, body shape, genitals)
  • self-control
  • competence
  • courage
  • protectiveness
  • resilience

However, when I think of the kind of behaviors associated with the phrase “toxic masculinity”, I get a very different (and mostly incompatible) list:

  • sexism & misogyny
  • homophobia
  • bullying
  • excessive use of drugs & alcohol
  • macho toughness

I think this is the heart of the division I see between people reacting to this issue. When someone says “masculine”, which of these two lists pops up in their head? You can easily tell by the litmus test of these two videos.

What I find especially fascinating and useful, is to construct a similar list using the phrases “feminine” and “toxic femininity”. To my mind, the first list is nearly identical, while the second list has its own (and different) set of revolting behaviors.

My point, really, is that using deliberately leading phrases like “toxic masculinity” or “toxic femininity” doesn’t actually help what is really an admirable goal: to eliminate the specific nasty behaviors associated with those phrases. At best, they serve to stir up animosity and misunderstanding between people who probably have the same goals at heart. At worst, they create a completely useless debate between people wanting to define “masculine” as meaning the first list versus the second.

Instead, I would urge people to discard the “package deal”, and focus on the real problems specifically, and one-by-one: sexual harassment, homophobia, bullying, and all those other behaviors we should no longer tolerate as a rational, civil society.