git --rebase
the history

A solid git history

Have you ever caught yourself questioning what is a good git history? I am always questioning myself about this.

For me a decent git history has the following aspects:

And throughout this post, I will try to convince you, or at least try to present you what is the idea that I call a solid git history.

Now that you understand what I love to have on git history, let's dig into what makes me sad.

Let's break this down a bit, so you can get the concept and learn how to do it properly.

Commits that just tell me what was done

You do this kind of shit, don't you? Everyone does. I did a lot in the past, so I don't blame you. But it is time to stop.

I would say, that could be OK to just state what you did on the git message subject, AKA the first line you write, which should have 50 characters length maximum.
And then, on the paragraphs before this one, you should state why you did that change in the code. If your project is quite new and you just adding a model, for example, would be ok to have empty git log message, with just the subject stating that you just created your model for example.

But now, let me tell you a story, after working on legacy code and dig into some crazy logic, I was very confused about it, and I tried to resort the git logs about it what was going on the code, and guess what? Most of it was just, refactoring X, removing Y and adding Z.

This was a Ruby project, I am a Ruby programmer, I know how to read and write Ruby code, and probably everyone who ever read those git logs is a Ruby programmer as well. So, we as Ruby programmers we KNOW when you are adding, refactoring or removing a code. You don't need to state that in your git log message. We definitely can read that using git and our knowledge of the language itself.

What I really wanna know is WHY you did that change. Which circumstances led you to make that change. This is the most important thing that should be stated in the git commit message.

I recommend you read this post too: https://chris.beams.io/posts/git-commit/

Remove debugger commits

This kind of things happen. When you are tired and you just want to get rid of that code you are working on. You add a few debuggers throughout the code, finally realize what is happening, fix, commit and push.

Then someone reviewing your code finds this debugger sitting there, what you generally do? You add a new commit "removing debugger", right? Please don't do it.

If you just committed your code, and you forgot to remove a debugger, instead of creating a new commit removing the debugger line use amend with no edit options.

Here is a step by step example how to do it:

        
$ git diff
diff --git a/app/models/lead.rb b/app/models/lead.rb
index 5bbd6df..d0e73b7 100644
--- a/app/models/lead.rb
+++ b/app/models/lead.rb
@@ -7,7 +7,6 @@ class Lead < ApplicationRecord
   private

   def send_notificaiton
-    require 'pry'; binding.pry
     LeadMailer.notification(self).deliver
   end
  end
        
      

This is the current diff, which shows we just removed our debugger.

        
$ git status
On branch debugger
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

      modified:   app/models/lead.rb

no changes added to commit (use "git add" and/or "git commit -a")

        
      

This shows that your branch is dirty, and you need to commit that.

        
$ git commit -am 'removing debugger'
        
      

Never add commits like this one

        
$ git commit -a --amend --no-edit
        
      

Instead just commit using --amend and --no-edit.

This will add your changes (debugger removal in this case) into the latest commit. It will also change the commit hash, so you may need to use force when you are pushing into origin. So, don't forget to gently push using --force-with-lease .

Fix tests commits

Ok, it may happen to you some failing specs on CI, you broke it. Then you just add a new commit fixing the specs? Nah, you can do better than that, I am very sure of it.

In my opinion, every single commit can run all specs and lints without fail. But could happen for some reason, such as randomizing your tests, your latest commit may fail on your CI pipeline. If you can reproduce the failure locally and fix your specs then do the same as above, commit with --amend --no-edit and you are good to go.

Third party tools references inside commit messages

One thing that I truly HATE is when you have references about third-party tools inside the git log messages.

It happened to me once, I was working as a freelancer for a small startup. Back then I was reading some commits trying to understand what is going on in some parts of the system, and most of the commit messages were the following:

        
* c782028 Closes #923
* ab276f3 Fixes #921
* dafec90 Added because of #920
        
      

Never do it

You can be using Jira to manage your tasks today, and if you link a Jira task on a git log message it will work, while you have a Jira account up and running. But let's say that in the future you migrated all your task organizations to a different tool, and you don't have your Jira account anymore. In the future, another programmer will need the information that you didn't state on the git message because you referenced the information into a third party tool that could not even exist anymore.

Changes due pull request discussions, stacking up each other

We discuss a lot over a new code before it gets into master (or staging) branch. Those changes could be small ones, change method name or fix a typo are good examples of small ones. The discussions sometimes are huge and lead you to a completely different solution in the end, with different classes, patterns, files and etc.

So, even when this kind of huge discussions happens you should not simply add git commits on top of your current implementation. If you have several commits, grouping small parts of your system on each commit you should manually rebase, and squash those commits in the right place.

I have an example here. Let's say that you have 3 commits. And then you open a Pull Request to the master branch of your project, the changes you are adding is the following:

        
$ git log --raw

commit 05241c56ed7fe550d4b0091af8b1831fc8ddbf0e (HEAD -> posts)
Author: Rubens Stulzer <rubens@stulzer.com>
Date:   Sun Nov 12 12:10:11 2017 +0100

    Adds post excerpt helper

:100644 100644 a7b8cec... b2844f0... M  app/helpers/posts_helper.rb
:100644 100644 dd12307... 2c47743... M  app/views/posts/index.html.erb
:100644 100644 f3d00cb... 514de84... M  spec/helpers/posts_helper_spec.rb

commit 92992834a45c843a3fd2d558caca2c434e0f4c62
Author: Rubens Stulzer <rubens@stulzer.com>
Date:   Sun Nov 12 11:52:25 2017 +0100

    Scaffolds posts

:000000 100644 0000000... 24f83d1... A  app/assets/javascripts/posts.coffee
:000000 100644 0000000... 1a7e153... A  app/assets/stylesheets/posts.scss
:000000 100644 0000000... 6045188... A  app/assets/stylesheets/scaffolds.scss
:000000 100644 0000000... 16bba99... A  app/controllers/posts_controller.rb
:000000 100644 0000000... a7b8cec... A  app/helpers/posts_helper.rb
:000000 100644 0000000... 0088c81... A  app/views/posts/_form.html.erb
:000000 100644 0000000... 2d5d444... A  app/views/posts/_post.json.jbuilder
:000000 100644 0000000... ded33f7... A  app/views/posts/edit.html.erb
:000000 100644 0000000... dd12307... A  app/views/posts/index.html.erb
:000000 100644 0000000... d4d0249... A  app/views/posts/index.json.jbuilder
:000000 100644 0000000... fb1e2a1... A  app/views/posts/new.html.erb
:000000 100644 0000000... c14ef10... A  app/views/posts/show.html.erb
:000000 100644 0000000... 5274482... A  app/views/posts/show.json.jbuilder
:100644 100644 787824f... aca042e... M  config/routes.rb
:000000 100644 0000000... fc22149... A  spec/controllers/posts_controller_spec.rb
:000000 100644 0000000... f3d00cb... A  spec/helpers/posts_helper_spec.rb
:000000 100644 0000000... 7bbf3bc... A  spec/requests/posts_spec.rb
:000000 100644 0000000... c691c1f... A  spec/routing/posts_routing_spec.rb
:000000 100644 0000000... 9867d20... A  spec/views/posts/edit.html.erb_spec.rb
:000000 100644 0000000... 98056b8... A  spec/views/posts/index.html.erb_spec.rb
:000000 100644 0000000... 0c44e27... A  spec/views/posts/new.html.erb_spec.rb
:000000 100644 0000000... ce1b1ae... A  spec/views/posts/show.html.erb_spec.rb

commit b9483cdfc4b4834a8f55054369a1f9e01673db41
Author: Rubens Stulzer <rubens@stulzer.com>
Date:   Sun Nov 12 11:44:16 2017 +0100

    Creates post model

:000000 100644 0000000... a06c91c... A  app/models/post.rb
:000000 100644 0000000... b788575... A  db/migrate/20171112103335_create_posts.rb
:000000 100644 0000000... d9ea0e8... A  db/schema.rb
:000000 100644 0000000... e0250cd... A  spec/models/post_spec.rb
        
      

Lots of files being added, right?

But basically, you have 3 commits telling me something here

Great, you think your history is solid, and your code is good. Which probably is true, until you open a Pull Request and someone asks you to change something related to the model, and another small thing related to the helper you added, and so on and on. Then, another person asks you to remove a bunch of code from your controller and its specs.

What I have seen throughout my life is people stacking commits, up each other in a gigantic loop, where you lost track of your history about what is going on in your feature branch.

Moving further, let's imagine the pull request you opened you agreed to change something like the description below

You have to use factory_bot instead of just create regular rails models in your specs

Here you need to change some code related to the commit "Creates post model", and probably add a different commit to install factory_bot itself.

        
RSpec.describe Post, type: :model do
  describe '#valid?' do
    subject(:valid) do
      described_class.new(title: title, body: body).valid?
    end

    ...


  end
end
        
      

You want to move from something like this

        
RSpec.describe Post, type: :model do
  describe '#valid?' do
    subject(:valid) do
      FactoryBot.build(:post, title: title, body: body).valid?
    end

    ...

  end
end
        
      

To something like this

To achieve this you need to first install the factory_bot gem, bundle it, generate the factory for posts, and then go to spec/models/post_spec.rb file and actually change it.

After you do all that, you will end up with something like this:

        
$ git status

On branch posts
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   Gemfile
        modified:   Gemfile.lock
        modified:   spec/models/post_spec.rb

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        spec/factories/

no changes added to commit (use "git add" and/or "git commit -a")
        
      

Here you have all files related to the factory_bot gem installation, and the changes to actually use the factory inside of posts model specs

        
$ git commit -am 'Adding FactoryBot'
        
      

I know that you want to do it. But please, let's move forward in our git knowledge

What you should actually do now, is two commits.

This is how to do it.

        
$ git add Gemfile Gemfile.lock

$ git commit -m 'Installs factory bot gem'
[posts d5bc3e3] Installs factory bot gem
 2 files changed, 8 insertions(+)

$ git status
On branch posts
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   spec/models/post_spec.rb

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        spec/factories/

no changes added to commit (use "git add" and/or "git commit -a")

$ git add spec/models/ spec/factories/

$ git commit -m 'should be squashed with posts model commit'
[posts 289ccaf] should be squashed with posts model commit
 2 files changed, 7 insertions(+), 3 deletions(-)
 create mode 100644 spec/factories/posts.rb

$ git log --pretty=oneline
289ccaf90f2620e41422213057b3ad48a3a8216d (HEAD -> posts) should be squashed with posts model commit
d5bc3e391c495de16f5f3c3f780d7bcd814af5a4 Installs factory bot gem
05241c56ed7fe550d4b0091af8b1831fc8ddbf0e Adds post excerpt helper
92992834a45c843a3fd2d558caca2c434e0f4c62 Scaffolds posts
b9483cdfc4b4834a8f55054369a1f9e01673db41 Creates post model

        
      

Now you have a bad history, but we can fix that

Now with these two commits, you will rewrite the history. Basically, you will move the Installs factory bot gem bellow the Creates post model. And the should be squashed with posts model commit will be squashed, into a single commit with Creates post model.

It sounds hard but is very easy to do it. In summary, we will interactively rebase the last five commits, change the other commits as we want, and squash commits that need to be squashed.

        
$ git rebase -i HEAD~5
        
      

This is basically saying to git that you want to rebase interactively the last 5 commits (HEAD~5)

The command will also prompt your text editor with the following content

        
pick b9483cd Creates post model
pick e299283 Scaffolds posts
pick b5241c5 Adds post excerpt helper
pick d5bc3e3 Installs factory bot gem
pick c89ccaf should be squashed with posts model commit

# Rebase 9500e50..289ccaf onto 9500e50 (5 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
        
      

Take your time, and read what is commented bellow your commits list

        
pick d5bc3e3 Installs factory bot gem
pick b9483cd Creates post model
fixup c89ccaf should be squashed with posts model commit
pick e299283 Scaffolds posts
pick b5241c5 Adds post excerpt helper
        
      

This is what I want, my history should be install factory bot, and then use it on the post model spec.

You are probably thinking now, "you told me that I should squash the commits into one, but you end up using fixup, why?"

Yeah, indeed, but fixup is simply squash without the commit message. If I used squash, git would stop the rebase and ask me to change the commit message, but I don't really want that, I just want to squash both commits into one.

After finish, your commits editing, save and quit. You will see something like this:

        
git rebase -i HEAD~5
Successfully rebased and updated refs/heads/posts.

$ git log --pretty=oneline
39270b439fb85878eebf97c1e13d2696b9e49b94 (HEAD -> posts) Adds post excerpt helper
705156a6c077e8d663d92c8ab5013a417a39139d Scaffolds posts
bc4220329994576c7127f01e3a8c7ebe04877f22 Creates post model
2fe183efb1af907e67cfb1daa216b6da42e74093 Installs factory bot gem
        
      

Now you end up with 4 commits that tell you a history

That is all folks, I hope I convinced you to do the right things on your next git commits. If you have any comments, please hit me up on twitter or something. You can find my info clicking here.