loading...
Cover image for Up your Git game and clean up your history

Up your Git game and clean up your history

christopherkade profile image Christopher Kade Updated on ・7 min read

This post is aimed for people who want to learn how to use commands such as rebase & learn a few tricks to have a nicer Git experience.

Alright, let's start by watching the Git history of some of my first ever projects on Github:

git history 01

Oh no

git history 02

Oh no no no no

git history 03

OH LORD WHAT HAVE I DONE

Now that I've ridiculed myself, let's check out what most of my projects look like now πŸ˜„

git history 04

Oof, much better

If you don't know what this all means, let me explain briefly:

This is a representation of your Git history on Github, meaning the commits you've done over time on your project's branches.

You can see your own by navigating to the "Insights" tab of one of your projects, then going to the "Network" tab on the left.

The black bar represents my master branch and each alternating blue & green bars are separate branches.

As you can see on the more recent and nicer history, they alternately get merged into master, creating this nice flow of writing code & merging it (which is always recommended, instead of accumulating pull requests).

So, how can you have a cleaner history? Let's go over real use-cases.

Here's what we're covering today (make sure to do each section in order):

The magic of git rebase

Official documentation

Git rebase lets your remodel your history to your will. See it as a way to manipulate your list of commits on a given branch.

For example, you could drop commits completely (basically say goodbye to them in the abyss of git), rename them (rewriting your commit message), squash them into other commits (which is useful to hide commits that do small things such as adding a semicolon, you don't really want to see them in your history) and many more things.

Learning by practice

Go to the following project I made for the occasion, fork it and let's get started.

Forking is basically creating a copy of my project for yourself, you'll be able to mess around with it without any problems ! To do it, click on the fork button on the top right:

fork button

Then, clone the repository you forked.

Example 1: fixing up a commit with rebase

Scenario: you have committed something that does not deserve a commit of its own, or you want to reduce the number of commits on your branch to one before making a pull request.

  • From the master branch, create a new branch.

  • Create a new file, its content doesn't really matter.

  • Commit that new file to your branch.

git add index.js
git commit -m "add index.js"
  • Update something in that file

  • Commit it again with a message such as "update index.js"

  • Run git log, as you can see, we now have 2 commits

We now want to fixup the update commit into the add commit, because this small change does not deserve a commit of its own.

To do so, we'll use the interactive mode of git rebase, which lets us apply the rebasing with a nice interface.

  • Run the rebase command like so:
git rebase -i HEAD~2

HEAD~2 means start from the last commit on the branch (the head) and go back 2 commits. If we wanted to manipulate more commits, we could change the value to the far right.

You should now have an interface that looks something like this:

rebase interactive

Don't panic, this only shows you the two commits you are changing at the top, and the available commands bellow them.

By default, the rebase interface uses Vim, to write in it, simply press the i key. You are now in "INSERT" mode. As we want to fixup the second commit in the first one, all we have to do is write fixup or f instead of pick in front of it. Our update index.js commit will now be squashed into the add index.js but only the add index.js's message will be kept.

  • Update the second line like so:
pick c0091ec add index.js
f a19336e update index.js

Now, we want to apply the rebase, press escape to leave the INSERT mode, press : (colon) and enter wq for "write" and "quit" and press ENTER to apply these changes. The colon simply allows you to write commands for Vim to execute.

The following message should now appear in your console:

Successfully rebased and updated refs/heads/{YOUR BRANCH NAME}.

Check your git log, you now have one beautiful and clean commit !

  • Finally, force push to that branch to apply the rebase to the remote server
git push origin {BRANCH-NAME} -f

The -f is essential as a rebase modifies your git history and requires to be forced.

Example 2: dropping a commit

These next 2 steps will be extremely similar to the first one because you now have the tools to do any kind of rebasing πŸŽ‰

Scenario: you want to completely remove a commit

We'll drop the add FILENAME commit we previously made:

  • Run the rebase command
git rebase -i HEAD~1
  • Add a d or drop in front of the commit you wish to drop.

rebase drop

  • Run :wq in your Vim editor (and check with git log that the commit was dropped)

  • Don't forget to force push it to your remote server πŸ˜€

Example 3: rewording a commit

Pretty similar, with one change.

Scenario: you want to fix a typo or rewrite a commit's title or description

  • Create a random commit

git reword

  • Run the rebase command
git rebase -i HEAD~1
  • Add a r or reword in front of the commit you wish to reword (no need to edit the title now).

  • Run :wq in your Vim editor. This will open a similar editor with the commit(s) you wish to reword.

git reword 2

  • Update the commit's title and description to your will, run :wq and that's it ! Check with git log that the rewording was applied

git reword 3

  • Don't forget to force push it to your remote server πŸ˜€

Example 4: rebasing on master

This example isn't reproduced in the Github project, but feel free to test it out.

Scenario: you have multiple PRs (Pull Requests) open at the same time

rebase master screenshot

You merge one PR, now, your second PR is not up to date with master, oh no !

rebase master screenshot 2

This very frequent scenario will have us rebase our second PR on master so that it gets the new code merged from the first PR.

  • From the branch you want to rebase (in our case, the second PR's branch), run the following:
git fetch

This downloads all the references our branch needs to apply the rebase.

  • Then, execute the rebase like so:
git rebase origin/master
  • Finally, run a git push origin {MY-BRANCH} -f to apply the rebase to our remote server

rebase master screenshot 3

Hurray !

Bonus: a better git log

Is your git log too much to handle?

git log screenshot

Would you rather have a git log that is straight to the point and looks nicer?

git log screenshot 2

Look no further ! Here's how you can achieve it, in your console, paste the following:

git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

You've now created an alias to git log called git lg that will display the nicer output showed before. Try it out by typing git lg (or git lg -p to see the lines that have changed).

Thanks to Coderwall for this bonus ✨

On the dangers of force pushing & other things to note

Force pushing can be dangerous when working with a team as mentioned by Nick Huanca:



<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">

Thanks for the article! It was a nice read/refresher. Since the audience is for people wanting to up their git game, I would suggest adding some messaging around the dangers of "force push" and maybe reference force push with care which leverages --force-with-lease so people don't accidentally overwrite team pushes. :)

Another noteworthy thing might be to git rebase --abort if things go unexpectedly sideways during a rebase (conflicts or other strange/unexpected behavior). It's nice to know, especially when getting started, how to back out of a command safely.

Thanks again!



So another alternative may be --force-with-lease which "allows one to force push without the risk of unintentionally overwriting someone else’s work. It will update remote references only if it has the same value as the remote-tracking branch we have locally." Reference. Thanks for the tip Nick ! πŸ˜„

As you may have noted he also mentions git rebase --abort, which allows us to stop a rebasing mid-way through in case anything goes wrong. So if for any reason you mess up your rebase, git it a little --abort and start again.

Closing thoughts

Learning how to use Git is probably one of the most important skills we can acquire as developers. I hope some of you won't be as afraid of rebase as I was a while ago.

If this post has been helpful, please feel free to give it a β€οΈπŸ¦„πŸ”– and to follow me on Twitter @christo_kade !

Posted on by:

christopherkade profile

Christopher Kade

@christopherkade

Web consultant @ZenikaIT // I sometimes record podcasts, build things and write about the web πŸ‘‹

Discussion

pic
Editor guide
 

Rewriting history on solo projects is fine but as soon as you work with another person you should consider it extremely harmful to do so on non-local branches.

From git-scm.com/book/en/v2/Git-Tools-R...

Don’t push your work until you’re happy with it
One of the cardinal rules of Git is that, since so much work is local within your clone, you have a great deal of freedom to rewrite your history locally. However, once you push your work, it is a different story entirely, and you should consider pushed work as final unless you have good reason to change it. In short, you should avoid pushing your work until you’re happy with it and ready to share it with the rest of the world.

 

Correct me if I'm wrong, but I didn't get the impression from reading this article that the author suggests we should be editing the history of the master branch or any other shared branch. I'm more under the impression that it's about tidying up your own fix or feature branch shortly before it goes into master, and I do not see any harm or problem with that, even in a team.

To the contrary, the benefit of a cleaner, more readable history that removes all clutter such as quick and dirty in-between commits and iterations that didn't end up in the final result, should bring all contributions into a sensible timeline that is easier to follow.

Talking about big teams, here's the stance from Phabricator's development team, a development and collaboration tool that originated from Facebook and turned into an Open Source project:

A strategy where one idea is one commit has no real advantage over any other strategy until your repository hits a velocity where it becomes critical. In particular:

Essentially all operations against the master/remote repository are about ideas, not commits. When one idea is many commits, everything you do is more complicated because you need to figure out which commits represent an idea ("the foo widget is broken, what do I need to revert?") or what idea is ultimately represented by a commit ("commit af3291029 makes no sense, what goal is this change trying to accomplish?").

"One idea = one commit" means that the entirety of the fix or feature branch should be squashed into a single commit in the mainline branch. A single commit that easy to apply and revert if necessary. All in-between steps should be removed. The proposed solution to this issue is:

...squashing checkpoint commits as you go (with git commit --amend) or before pushing (with git rebase -i or git merge --squash), or having a strict policy where your master/trunk contains only merge commits and each is a merge between the old master and a branch which represents a single idea

Source

Several other articles online give the same idea that squashing and rebasing is the way to go, especially when you work in teams:

Editing the history is only problematic when the editor doesn't 100% know what they are doing, and even then, destructive changes can be prevented by simply setting up the git repository server to reject destructive changes to the shared mainline / development branches.

On GitHub you can set up branch protection rules to prevent any push --force to any branch of your choice from being accepted. Phabricator doesn't allow "destructive changes" to any branch by default. Bitbucket offers the same protection under "branch permissions".

Practically speaking there is nothing to worry about with proper repository configuration and with the right education. And articles like this one are helpful

 

I didn't get the impression from reading this article that the author suggests we should be editing the history of the master branch or any other shared branch

Absolutely, that was not my intention, if it wasn't clear then that's my bad.

On GitHub you can set up branch protection rules to prevent any push --force to any branch of your choice from being accepted.

Yeah, I don't see an instance where force pushing to master would be recommended, glad you mentioned these protections for others to know about.

And thanks for sharing some resources, I'm glad you commented πŸ˜„

 

Hi yes I agree there's nothing in the article that says rewriting master is a good idea, however I felt it was important to state that there are problems. Rewriting history and rebasing are not silver bullets and can cause issues without proper procedure.

Admittedly reading my comment back it sounds like I've dismissed the whole article on that basis but that was absolutely not my intention!

 

I totally agree with your statement, all these manipulations should be done locally before pushing your work. πŸ˜€

 

"Fixing" history is a misguided (and dishonest) effort--a waste of time. Rebase is a powerful git tool and should be learned and used appropriately, but maintaining a useful git log should be accomplished with forethought and disciplined adherence to good commit practices. That said, I'd rather have an imperfect but unmolested git history. A "clean" history is just a lie.

 

Bs. Clean history represent a step by step application growing and not how u or u team work. So isnt lie. No sense of blame anybody only the log of the build from first line to current.

 

Nice walkthrough. Rebasing is often painted as advanced, but I think it's best to play with it early on, and not fear it later.

Git rebase lets your remodel your history to your will. See it as a way to manipulate your list of commits on a given branch.
...
To do so, we'll use the interactive mode of git rebase, which lets us apply the rebasing with a nice interface.

Technically true, but I wouldn't phrase it like that. IMO, taken together these risk implying untrue things β€” that a branch's history can be arbitrarily rewritten with a plain rebase, that it's why rebase exists, and that --interactive just changes the UI.

I'd instead say:

git rebase allows you to replay the changes introduced in a set of commits on top of a specified base. It works by repeated cherry-picking (i.e. applying changes introduced by a commit on top of a different one). --interactive allows you to edit the changes before they're applied.

This leads to a special form of rebase β€” interactive rebase where the source branch is also the target β€” where you can arbitrarily rewrite a branch's history.

git-scm.com: Rewriting History


This very frequent scenario will have us rebase our second PR on master so that it gets the new code merged from the first PR.

Just to add context: rebasing works here, but merging master is likely better for PRs on a team.

  • if there are conflicts, you'll only have to fix them once, rather than repeatedly across commits
  • rebasing can cause reviewers to lose work
  • you can't assume that others haven't checked out a PR branch
  • it's the truth

I'd say only the first two are potentially serious, but I can't think of an upside that'd make it worth dealing with them.

 

Thanks for the article! It was a nice read/refresher. Since the audience is for people wanting to up their git game, I would suggest adding some messaging around the dangers of "force push" and maybe reference force push with care which leverages --force-with-lease so people don't accidentally overwrite team pushes. :)

Another noteworthy thing might be to git rebase --abort if things go unexpectedly sideways during a rebase (conflicts or other strange/unexpected behavior). It's nice to know, especially when getting started, how to back out of a command safely.

Thanks again!

 

Great points Nick, thank you for sharing them. I did not know of --force-with-lease.

I've added a section called "On the dangers of force pushing & other things to note" which mentions your comment.

Thanks again πŸ™‚

 

More often than not I've got one of those commit histories that look like the "OH LORD WHAT HAVE I DONE" graph, and even rebase is hard to use to clean up -- this happens most often when I have something that fixes one thing, but needed two or more changes. (I really should get into the habit of committing after each file change, even if it leaves HEAD broken.)

In those cases, I use a process of checking out master into a cleanup branch, git cherry-pick -n (i.e., don't commit) the changes from the branch I'm cleaning up onto the cleanup branch, and then git reset. Now all of the commits are squashed on a per-file basis. I then repeatedly use git add -p to build a new set of per-file commits that catch me up by replying y until the next change would go into a new file. Then commit and repeat until all the changes have been built into commits.

It loses the commit order, but very nicely collects the changes into per-file changes. Hm. This might have been better as a post instead of a comment!

 

Bro, thank you. This article has been a great help for me today.
I messed things up with git reset HEAD~. Luckily, gitlense(vscode ext) had a copy of the files that vanished. After doing the necessary changes, i did a rebase to fix up the previous commit as you mentioned. πŸ‘

 

Thanks for the post, very good practical examples to try rebasing.

I'm just curious how did you achieve this lonely commit? πŸ˜€
lonely commit

Example 3: rewording a commit

I think interactive rebase in this example is overkill.

git commit -- amend -m "new message" will do the work.
But changing a message for commit head~2 is impossible with amend, so interactive rebase with reword will solve such tasks perfectly.

 

With pleasure, super glad it helped.

This last commit is just an automatic commit made to deploy to Github Pages

github pages

A whole different thing, so don't worry about it !

Thanks for mentioning amend πŸ˜„

 

Hi Christopher,
Nice article, many thx!

If I may suggest, instead of fetch remote parent branch, I used to use git pull --rebase cmd.
For example, if I need to rebase 'develop' branch from master, I'll do git pull --rebase origin master.
This prevent me to forget about fetching the HEAD of master every time I need to rebase.

Another point, if you really need to rewrite history of a project, I would suggest to learn git reflog cmd also. It's really convenient to recover deleted commit for example, or to cancel a mishandling rebase...

Thx again

 

Hey man, great article and especially great title choice! I came from a dev.to mail and upon reading it I immediately thought: this is about rebasing.
The Table of Contents anchors don't work though, had to scroll manually

 

Thanks for the great article! Is rebasing/squashing only useful for auditing your git history and/or making it easier to read?
I guess what I'm asking is - if I recommend doing this to my team, what are some of the practical applications I can mention to get them on board?

 

Isn't the combo of git fetch and git rebase origin/master equivalent to git pull --rebase origin/master?

 

This was very helpful thank you

 

Thank you Christopher Kade !

 

Many thanks for this excellent article, something I needed to get more comfortable with Rebase. Much appreciated!