Intentional commits is the term I use for habits and workflows that help with creating Git commits1 exactly the way we intend to. The opposite are incidental commits, with changes that “just so happened”. For many developers, even quite experiences ones, Git can feel a bit like black magic and can be a daunting and scary tool. Most developers have seen or caused commits with unintended changes; I certainly have, many times in fact when I was relatively new to Git.
Here are some common examples of mistakes that I see all the time which illustrate what an unintentional or incidental commit might look like:
- Temporary log statements or temporarily commented-out code left over from a recent debugging session.
- Generated or accidentally created files.
- Committing on the wrong branch.2
- Creating unnecessary merge commits due to what I call blind pulls.3
- Committing multiple changes at once because we forgot to commit and got dragged into a rabbit hole of unrelated changes.
Intentional commits go much further than just preventing common mistakes, and I will cover aspects like intentional scope in separate articles. This one is about how we can get continuous feedback while preparing a commit.
Are good intentions enough?
People are fallible, and we all make mistakes. When something goes wrong, simply saying “try harder” rarely works. Often there is a disconnect between the intention and the outcome, and increasing the intention doesn’t magically lead to better results—more likely it just leads to more frustration. That’s why, instead of telling my coaching clients and mentees to just “be more intentional”, I’m more interested in specific easy-to-adopt habits that translate the intention into the intended result.
Don’t commit with blindfolds on
I mean metaphorical blindfolds, of course. I’m referring to the many developers (probably the majority) that I see who, if they are ready to commit their changes, do something like git commit -a (or git add . followed by git commit). The equivalent in GUI-based Git clients happens just as often, as there is usually some button or checkbox to “stage all”.
Let’s say I make a harmless code change, then I want to commit it, so I do this:
git add .
git commit -m "Print Hello World to stdout"
Nothing suspicious so far. I know this is how many developers commit their changes.
Let’s look at the diff:
--- /dev/null
+++ b/HelloWorld.java
@@ -0,0 +1,6 @@
+public class HelloWorld {
+
+ public static void main(String[] args) {
+ System.out.println("Hello, world!");
+ }
+}
--- a/ImportantProductionCode.java
+++ b/ImportantProductionCode.java
@@ -1,7 +1,9 @@
public class ImportantProductionCode {
public void initialize() {
- initializeAirbagSensors();
+ // TODO Don't forget to uncomment!
+ // Temporarily commented out for local debugging.
+ //initializeAirbagSensors();
initializeEmergencyBrakes();
}
Oops! Looks like we accidentally disabled the initialization of the airbag sensors. Not good! Of course this is an extreme hypothetical example, but in many cases the consequences can be at least some wasted time from necessary rework or even reputational damage due to defects in production. (Yes, I’ve seen similar situations many times.)
Doing git add . followed by git commit is like being blindfolded. We stage everything in our working tree without any feedback as to which changes are there, and we commit without any feedback what we staged. Remember that commit means that we really stand behind these changes. Staging (i.e., git add) is the process of preparing the commit before finalizing it into an immutable commit object. It’s like saying these are the changes that I intend to put into a snapshot of the code base, potentially forever. That requires us to know what these changes actually are.
So, then what does the intentional version of this look like?
The precondition is that we have an intention at all. I don’t mean intentionality per se in the sense of being fully concentrated4, but just that we have a reason for our commit. In the example above, the intention is obvious, and described very well by the commit message: We wanted to print “Hello, world!” to the console. So it’s also obvious which changes we intended to commit. This is the intention behind our commit. Note that the intention is not always that obvious. Many developers have a habit (or lack of habits?) of just working on the code, making different changes, and at some point they commit to “back up” their changes, just to prevent potential loss of their work. That means that at the point in time where they are ready to commit their changes, the intention is not clear anymore. I will go further into how to tackle these problems another time when I focus on intentional commit scope.
Then, if we know what we intend to commit and why, the first step is to look at what we actually changed. With git status we get a high-level overview of the files we changed:
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directo
ry)
modified: ImportantProductionCode.java
Untracked files:
(use "git add <file>..." to include in what will be committed)
HelloWorld.java
no changes added to commit (use "git add" and/or "git commit -a")
If I had done this before staging everything with git add ., I would have seen that there are changes in ImportantProductionCode, which is not intended to be in this commit. I would have worked without blindfolds on. And then I could have done git add HelloWorld.java explicitly, rolled back or stashed the rest and after another quick run of the tests I would have made my intentional commit.
Building muscle memory
Before we look into more commands, let me say this: I’m generally not a fan of so called “cheat sheets” that just list some Git commands and explain what they do. I’ve never seen that result in individual developers using more Git commands, let alone using them more effectively. The need to use a certain Git command (be it in a terminal or in an IDE) should always arise from the intended effect (e.g., checking whether we staged the correct changes); having a list of commands rarely leads to recognizing the need for a certain command when the situation comes.
Instead, if you want to memorize something, let it be this: status, stage, verify, commit. These are the four steps of creating an intentional commit. First, we check the current status: Which changes have I made? Are these changes intended? Then we stage, either everything or a subset of the stages. Next, we verify that we staged the correct changes, and that the currently staged changes are the intended contents of the commit. Verifying can also include stashing the remaining (unstaged) changes and running the tests again. Lastly we do the commit and push it. Reading this it might seem tedious and additional work, but it usually takes about 5 to 10 seconds. One of many variants, depending on the situation, could look like this:
git status
git add .
git diff --staged
git commit
More situational commands for intentional commits
I use the following commands probably almost as often as git status, but I see far fewer developers use these. The next stage after blindly staging all changes that I see in many developers is to use git status and then git add ., which is a step in the right direction, but it only prevents accidental staging of wrong files, not on a per-line basis.
git diffgit diff --staged
This lets me see the diff of the changes at a glance. Which lines I’ve changed/added/removed. With the --staged option, I see the diff of the staged changes, i.e., the changes that will actually be part of the commit. This is especially helpful when I stage only a subset of my current changes (which for me is quite rare, since I commit frequently, but it does happen occasionally).
For example, let’s say I staged the HelloWorld file using git add HelloWorld.java. Then git diff shows me the unstaged/unintended changes:
$ git diff
diff --git a/ImportantProductionCode.java b/ImportantProductionCode.
java
index 46fcce9..5dcc84e 100644
--- a/ImportantProductionCode.java
+++ b/ImportantProductionCode.java
@@ -1,7 +1,9 @@
public class ImportantProductionCode {
public void initialize() {
- initializeAirbagSensors();
+ // TODO Don't forget to uncomment!
+ // Temporarily commented out for local debugging.
+ //initializeAirbagSensors();
initializeEmergencyBrakes();
}
git diff --staged shows me the intended changes that will be part of the commit:
$ git diff --staged
diff --git a/HelloWorld.java b/HelloWorld.java
new file mode 100644
index 0000000..ab5d894
--- /dev/null
+++ b/HelloWorld.java
@@ -0,0 +1,6 @@
+public class HelloWorld {
+
+ public static void main(String[] args) {
+ System.out.println("Hello, world!");
+ }
+}
This, by the way, is also where my self-review happens, as the diff makes it very easy spot typos, bad names, and structural issues, especially if the changes per commit are small.
git add -p
Patch staging, for when you only want to stage certain chunks (Git calls them hunks) of a file instead of a whole file. You will see the diff of individual chunks of code, making it easy to self-review your own code piece by piece. I recommend to look up the manual and try it out if you haven’t used it before. As mentioned earlier, the need to look up the right command or what a certain command does follows the intended goal. In this case, the goal is staging a part of a file, and if you’re never in that situation anymore, that might be a good sign.
git show
Show the message, meta data and diff of the last commit. Useful if you want to verify after committing and before pushing that you actually committed the correct changes. Especially helpful when just starting to get more confident with intentional commits, to double check the result.
There are many more commands that can be helpful depending on the situation, but memorizing a number of commands is not very effective. Instead, try to look for more ways to do status, stage, verify, commit, and depending on the situation look up the commands as needed.
Conclusion
Note that this is just a quick sanity check that allows me to never commit unintended changes. Once it becomes muscle memory, typing these 2 or 3 commands before a commit takes me at most 10 seconds, especially since the changes per commit are quite small and I know ahead of time which commit scope I’m working on. So the whole process of committing should take maybe 30 seconds up to 2 minutes, including a sanity self-review of the diff and typing of the commit message. The more we commit (pun intended) to our subconscious and to muscle memory, the more we can use our conscious mental capacity for higher-order strategic thinking, like what the next few commits should build up to, or how to shape the design with the next few commits. And as your Git fluency improves over time, the less likely it becomes that you actually have unintended changes in your working tree, because you will commit more frequently, intentionally, and confidently.
- The concept applies to version control commits in general, but I am most familiar with Git and I coach developers for effective Git usage specifically. ↩︎
- If you use multiple branches. Not that I recommend you to do so. ↩︎
- A blind pull is when you pull changes into your local topic/feature branch without knowing what you are pulling or whether you are even pulling anything at all. ↩︎
- As humans we have a limited capacity for highly concentrated work. That’s why it’s important to make the majority of our work so easy that the high-intensity focus can be concentrated on the difficult things, like decision making. Imagine for example taking a walk while thinking about a difficult problem, where the walking requires almost no mental capacity at all due to muscle memory. Creating commits the way we intend to should be like walking, so that the thinking can be focused on what to commit. ↩︎

