A useful commit answers one question about the behavior of a repository. It includes the implementation, the checks that establish that behavior, and documentation needed to use it. Smallness means a bounded reason for change, rather than a target number of lines. An inclusive shipping threshold might require edits to both a function and its regression test while leaving an unrelated draft outside the commit.

The index defines the checkpoint

Git distinguishes the working tree, the index, and the committed tree. Editing a file changes the working tree. Adding it copies its current content into the index. Further edits do not automatically update that staged version. A successful test against the working tree can therefore exercise code that the proposed commit does not contain. Inspect the staged diff, including deletions and new files, and test the selected snapshot when partial staging creates meaningful differences.

The index is particularly useful when two independent tasks share a checkout. Stage only the paths or hunks belonging to the current purpose. Before committing, check that required imports, fixtures, configuration, and tests are included. A tiny implementation patch without its required schema change is not an independently usable checkpoint. Conversely, a mechanical rename across many files can still express one coherent operation.

When it breaks

A mixed commit couples unrelated decisions. Reverting a visual change might remove a reliability fix hidden beside it. Splitting the files arbitrarily does not solve this when the first commit calls an API introduced only by the second. Aim for meaningful intermediate states, and state dependencies explicitly when a sequence must be reviewed together.

Small commits improve the information available to a reviewer; they do not guarantee conflict-free or operationally safe reversal. Later code may depend on an earlier behavior, and a completed database migration may have effects outside Git. A revert records a new inverse change and can conflict with intervening edits. Review and test the resulting state before deploying it.

Bisect also needs a reliable good-or-bad test at historical revisions. An uncompilable midpoint is not automatically evidence that the regression began there. Skipping untestable commits may leave several possible candidates. Keeping checkpoints executable makes the search more useful, but neither commit size nor a descriptive subject can repair a flaky test.

Check it quickly

Save the following as small-commits.py and run python3 small-commits.py with Git installed. It creates a temporary repository, uses an isolated Git configuration and empty hooks directory, and makes two local commits there. The application charges five units below a subtotal of fifty and should charge zero at fifty. The initial implementation incorrectly includes fifty in the charged range.

from pathlib import Path
from tempfile import TemporaryDirectory
import json
import os
import subprocess
import sys

with TemporaryDirectory() as directory:
    repo = Path(directory) / 'repo'
    repo.mkdir()
    hooks = Path(directory) / 'empty-hooks'
    hooks.mkdir()
    env = {'PATH': os.environ['PATH'], 'LC_ALL': 'C',
           'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': os.devnull}

    def git(*args):
        return subprocess.check_output([
            'git', '-c', 'user.name=Example', '-c', 'user.email=example@example.invalid',
            '-c', 'commit.gpgsign=false', '-c', f'core.hooksPath={hooks}',
            '-c', 'core.autocrlf=false', *args], cwd=repo, env=env, text=True)

    git('init', '-q', '-b', 'main')
    (repo / 'price.py').write_text('def shipping(subtotal):\n    return 5 if subtotal <= 50 else 0\n')
    (repo / 'test_price.py').write_text('from price import shipping\nassert shipping(49) == 5\n')
    (repo / 'notes.txt').write_text('unrelated draft\n')
    git('add', 'price.py', 'test_price.py', 'notes.txt')
    git('commit', '-qm', 'Add shipping calculation and its initial check')
    test = 'from price import shipping\nassert shipping(49) == 5\nassert shipping(50) == 0\n'
    (repo / 'test_price.py').write_text(test)
    failed = subprocess.run([sys.executable, '-B', 'test_price.py'], cwd=repo,
                            capture_output=True, text=True)
    assert failed.returncode != 0 and 'AssertionError' in failed.stderr
    (repo / 'price.py').write_text('def shipping(subtotal):\n    return 5 if subtotal < 50 else 0\n')
    (repo / 'notes.txt').write_text('unrelated draft, still being edited\n')
    git('add', 'price.py', 'test_price.py')
    staged = git('diff', '--cached', '--name-only').splitlines()
    assert staged == ['price.py', 'test_price.py']
    snapshot = Path(directory) / 'index-snapshot'
    snapshot.mkdir()
    for name in staged:
        (snapshot / name).write_text(git('show', ':' + name))
    subprocess.run([sys.executable, '-B', 'test_price.py'], cwd=snapshot, check=True)
    git('commit', '-qm', 'Make the free-shipping threshold inclusive')
    assert git('status', '--porcelain') == ' M notes.txt\n'
    assert git('show', 'HEAD:notes.txt') == 'unrelated draft\n'
    print(json.dumps({'regression_failed_before_fix': True,
                      'staged_files': staged, 'staged_test_passed': True,
                      'unrelated_edit_preserved': True}))

The new boundary assertion first fails against the old function. After the fix, only price.py and test_price.py enter the index. The script copies their staged bytes into a separate directory and runs the assertion there before committing. The JSON result reports both the earlier failure and the passing staged test.

The final status contains only the unrelated modification to notes.txt; its committed version still has the original text. That verifies selective staging without discarding another task’s work. The fixture tests one complete pair of files, not every possible partial-staging arrangement, and it does not perform a revert or a bisect session.

Write the reason that survives the diff

A subject such as “Make the free-shipping threshold inclusive” identifies the changed behavior. Explain the observed boundary error, the intended contract, and the relevant validation in the body when those details are not obvious. Avoid claiming that a test passed merely because a test file was added. Record which snapshot actually ran. Before publishing a sequence, read its combined diff and its individual checkpoints: the complete feature must work, and the intermediate story should accurately describe how it becomes usable.

Sources

  • Git: git-add, how the index records selected content, checked 2026-09-13.
  • Git: git-bisect, testable revisions and skipped commits, checked 2026-09-13.
  • Git: git-revert, reversal commits and conflicts, checked 2026-09-13.