The secret to a maintainable Git repository

If you think your commits are too small, make them smaller

Version control is one of the most powerful tools we have as developers, yet many teams only scratch the surface of its capabilities. A common pattern emerges in most codebases: large, monolithic commits that bundle many changes together, obscuring the evolution of the code and making it harder to maintain over time.

Understanding what makes a good commit isn’t just about following best practices – it’s about leveraging version control to make our lives as developers easier. By rethinking how we structure our commits, we can transform Git from a mere backup system into a powerful tool for understanding, debugging, and maintaining our code.

Why developers create large commits

The tendency to create large commits often comes from a misunderstanding of what commits represent. Many developers see them as milestones tied directly to business tasks or feature implementations. This leads to commits containing dozens of files with hundreds of changes, each one trying to encapsulate an entire feature or bug fix.

This approach seems logical at first glance. After all, isn’t a commit supposed to represent a complete piece of work? But this thinking confuses two distinct concepts: the logical grouping of changes needed to implement a feature (represented by a branch), and the individual steps needed to get there (represented by commits).

Understanding the role of commits

Think of commits like the steps in a cooking recipe. While the end goal might be to make a lasagna, each step - preparing the sauce, cooking the pasta, layering the ingredients - is distinct and self-contained. You wouldn’t want a recipe that just said “make lasagna” in one step.

Similarly, commits should represent logical, atomic changes to your codebase. The branch name and description can carry the context of the feature or bug fix, while each commit focuses on a specific, well-defined change:

# Instead of one large commit:
git commit -m "Implement user authentication feature"

# Break it down into meaningful steps:
git commit -m "Add password validation utilities"
git commit -m "Create user credentials database schema"
git commit -m "Add login form component"
git commit -m "Implement JWT token generation"
git commit -m "Add authentication middleware"

Each commit should ideally change one thing and do it well. This might mean updating a single component, refactoring a specific function, or adding a new utility class.

The power of git bisect

Small, focused commits unlock powerful debugging tools that become practically unusable with large commits. Git bisect is perhaps the most striking example.

Imagine you’ve discovered a bug in production, but you’re not sure when it was introduced. With git bisect, you can perform a binary search through your commit history to find the exact commit that introduced the problem:

# Start the bisect process
git bisect start
git bisect bad HEAD  # Current version is broken
git bisect good main  # Last known good version

# Git will checkout commits for you to test
# For each commit, mark it as good or bad
git bisect good  # or git bisect bad

# Once found, git will show you the problematic commit

However, this tool becomes much less useful with large commits. If a single commit contains hundreds of changes, identifying the specific change that introduced the bug becomes significantly harder. You’ve found the haystack, but you still need to find the needle.

Git blame that actually helps

Another powerful tool that benefits from small commits is git blame. When trying to understand why a particular line of code exists, git blame shows you the commit that last modified it.

With small, focused commits, git blame becomes invaluable:

# Large commit:
git blame somefile.js
# Shows: "Implement user authentication feature (includes form validation, database changes, and JWT)"

# vs. Small commits:
git blame somefile.js
# Shows: "Add password strength validation function"

The second example immediately tells you why that specific code exists, making it much easier to understand and modify when needed.

Dividing and conquering

Creating smaller commits requires a shift in your development workflow, but the benefits are substantial. Not only will your codebase be more maintainable, but you’ll likely find yourself moving faster. When you commit frequently, you’re always working with a small, manageable set of changes. This makes it easier to spot issues early, switch contexts when needed, and maintain your momentum.

Think of it like rock climbing: making small, secure moves lets you progress steadily and safely. Each hold you reach is a stable position. In contrast, attempting large moves is not only riskier but often slower, as you have to be much more careful and might need to backtrack if something goes wrong.

A working system. Always.

A crucial principle of small commits is that each one should leave your codebase in a working state. This means:

  • The code should compile
  • All tests should pass
  • The application should run
  • No partial features should be exposed to users

This might seem challenging at first, especially when implementing larger features. However, techniques like feature flags and careful refactoring make it entirely possible. For instance, when adding a new authentication system:

# Each commit maintains a working state:
git commit -m "Add password validation utilities with tests"
# (Code works, old auth still in place)

git commit -m "Create user credentials schema and migrations"
# (Database changes complete, old system still works)

git commit -m "Add new auth implementation behind feature flag"
# (New system exists but isn't active)

git commit -m "Switch to new auth system and clean up old code"
# (Feature flag removed, system fully migrated)

This approach provides several benefits:

  • You can run tests at any point in your history
  • Other developers can check out any commit and have a working system
  • Git bisect becomes much more reliable
  • Reverting changes is safer and more predictable

A more efficient workflow

Once you get into the rhythm of creating small, focused commits, you’ll likely find your development speed increasing. Instead of getting bogged down in large, complex changes, you’re always working with a manageable scope. This creates a virtuous cycle:

  • Small changes are easier to think about
  • Easier changes lead to more frequent commits
  • Frequent commits reduce the risk of losing work
  • Lower risk encourages more experimentation
  • More experimentation leads to better solutions

Keeping your history clean

Once you’re comfortable with small commits, you can take advantage of more advanced Git features:

# Squash commits if needed before merging
git rebase -i origin/main

# Fix up commits during development
git commit --fixup <commit-hash>
git rebase -i --autosquash origin/main

# Split commits that ended up too large
git rebase -i <commit-hash>~
# Mark the commit as 'edit' and use git reset HEAD^ to uncommit changes

These tools become much more useful when working with small, focused commits that each represent a single logical change.

Conclusion

Treating commits as small, atomic units of change rather than feature-level checkpoints transforms Git from a simple backup system into a powerful development tool. It makes your codebase easier to understand, debug, and maintain over time.

Keep in mind that the goal isn’t to create as many commits as possible, but to make each commit represent a single, logical change. Let your branch carry the context of what you’re building, and let your commits tell the story of how you built it.


Are you looking to improve your team’s Git practices? We’re passionate about helping development teams establish efficient version control workflows that stand the test of time. Whether you’re struggling with merge conflicts, wanting to implement better commit practices, or need guidance on Git workflows that scale, let’s talk! Reach out to us through our contact form or send us an email at contact [at] asyncsource.com. We’d love to share our experience and help you build a more maintainable codebase.

© AsyncSource - All rights reserved