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:
- Each commit tells me a history by itself
- Each commit runs all specs without fail
- Each commit runs all lints without fail
- Each commit tell why something was added/changed/refactored/removed
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.
- Commits that just tell me what was done
- Remove debugger commits
- Fix tests commits
- Third party tools references inside commit messages
- Changes due pull request discussions, stacking up each other
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
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
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:
Lots of files being added, right?
But basically, you have 3 commits telling me something here
- Adding a model, its specs and database migration
- Another huge commit adding all views, controller and routes related code
- Then a small commit with just a helper method to display something better
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
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.
- One installing FactoryBot
- And another should be squashed into the
Creates post model, where you will start using FactoryBot on the post model spec and will register the post factory
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
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.