DEV Community

Cover image for Engineering practice: git, code, tests · 1. Git as a thinking tool
Amit chakraborty
Amit chakraborty

Posted on Originally published at amitchakraborty.dev

Engineering practice: git, code, tests · 1. Git as a thinking tool

Engineering practice: git, code, tests · Chapter 1 of 12 · Craft · new chapter every Sunday night

By the end of this chapter: Write commits that explain a change to a stranger, including you in a year.

The problem

You are a junior engineer on a two‑week sprint for the payments team. Mid‑sprint you discover a flaky test that fails on the CI runner every night. You open a branch, add a one‑line guard clause, and push. The CI pipeline passes, so you think you’re done and type:

git commit -am "fix bug"
git push origin payment‑guard‑2024‑09‑15
Enter fullscreen mode Exit fullscreen mode

Two days later, a senior engineer on‑call receives a production alert that the new guard clause is causing a regression for a handful of customers. She blames the change, opens the PR, and sees only the terse message “fix bug”. She has to dig through the diff, ask you for context, and then spend an hour reproducing the scenario before deciding whether to revert. The rollback costs the team an hour of on‑call time, a delayed release, and a bruised confidence in the sprint’s quality.

The root cause isn’t the code; it’s the commit message that failed to explain what was changed, why it was changed, and how the change should be understood later. You need a disciplined way to write commits that a stranger – or you, a year from now – can read and act on without a chase.


Before you start

  • Git version: 2.42.0 (or any 2.30+ – the commands used are stable across recent releases). Verify with git --version.
  • Repository: a local clone of the service you are working on, with a remote named origin.
  • Commit‑message hook (optional but recommended): a simple commit-msg script that enforces a 72‑character body line limit. Create it once with:
  cat > .git/hooks/commit-msg <<'EOF'
  #!/bin/sh
  # Enforce body line length ≤ 72 characters
  awk 'NR>1 && length($0)>72 {print "Error: line " NR " exceeds 72 characters"; exit 1}' "$1"
  EOF
  chmod +x .git/hooks/commit-msg
Enter fullscreen mode Exit fullscreen mode

Run git commit --allow-empty -m "test"; you should see no error. If the hook fires, you’ll see the error line printed.

  • Editor: ensure core.editor is set (e.g., git config --global core.editor "vim"). Test with git commit on an empty repo; the editor should open.

If all the above succeed, you’re ready to adopt a reproducible commit‑message workflow.


Why a good commit message matters

What information should a commit convey?

A commit is a snapshot plus a story. The story must answer three questions:

  1. What changed? – a concise summary (≤50 characters) that can stand alone in logs.
  2. Why it changed – the motivation, ticket number, or bug description.
  3. How to understand the change – any side effects, migration steps, or roll‑back instructions.

When these three pieces are present, git log --oneline remains a useful timeline, and git show <sha> instantly tells a reviewer the intent behind the diff.

How to structure the message

The de‑facto standard (adopted by Linux, Git itself, and most large‑scale projects) is:

<type>(<scope>): <subject>

<body>

<footer>
Enter fullscreen mode Exit fullscreen mode
  • type – feat, fix, refactor, test, docs, etc.
  • scope – optional component name, e.g., payment, api, ui.
  • subject – imperative, ≤50 chars, no period.
  • body – wrapped at 72 chars, explains why and how.
  • footer – optional, for breaking changes (BREAKING CHANGE:) or ticket references (Closes #1234).

Example:

fix(payment): guard against zero‑amount invoices

The nightly test uncovered a case where invoices with amount 0
triggered a division‑by‑zero in the billing engine. Adding a guard
prevents the crash and aligns with the new validation rules.

Closes #4521
Enter fullscreen mode Exit fullscreen mode

When to amend vs. new commit

  • Amend (git commit --amend) – use when the previous commit is still the tip of your branch and you realise the message is incomplete before pushing. This rewrites history, keeping the diff intact.
  • New commit – use when the commit is already shared (pushed) or when you need to preserve the original context (e.g., a hotfix followed by a detailed explanation).

How to enforce consistency

  • Template – place a .gitmessage.txt in the repo root:
  type(scope): subject

  # Write a detailed description. Wrap at 72 characters.
  #
  # Footer (optional): e.g. Closes #1234
Enter fullscreen mode Exit fullscreen mode

Enable it with git config commit.template .gitmessage.txt.

  • Hook – the commit-msg hook above enforces line length; you can extend it to check the subject format with a regex.

Build it

Follow these steps to turn a vague commit into a clear, future‑proof one.

  1. Create a branch and make a change
   git checkout -b payment‑guard‑2024‑09‑15
   # edit src/payment/invoice.py, add guard clause
   git add src/payment/invoice.py
Enter fullscreen mode Exit fullscreen mode

Why: Working on a dedicated branch isolates the change.

  1. Stage the change and start a commit with the template
   git commit
Enter fullscreen mode Exit fullscreen mode

The editor opens with the contents of .gitmessage.txt. Fill in:

   fix(payment): guard against zero‑amount invoices

   The nightly test uncovered a case where invoices with amount 0
   triggered a division‑by‑zero in the billing engine. Adding a guard
   prevents the crash and aligns with the new validation rules.

   Closes #4521
Enter fullscreen mode Exit fullscreen mode

How to confirm: Save and quit. Git prints:

   [payment-guard-2024-09-15 abcdef1] fix(payment): guard against zero-amount invoices
    1 file changed, 2 insertions(+), 0 deletions(-)
Enter fullscreen mode Exit fullscreen mode

The commit SHA (abcdef1) appears; note it for later.

  1. Run the commit‑message hook (automatically invoked). If any line exceeds 72 characters, you’ll see:
   Error: line 7 exceeds 72 characters
Enter fullscreen mode Exit fullscreen mode

Edit the message to wrap at 72 chars, then repeat step 2.

  1. Push the branch
   git push -u origin payment-guard-2024-09-15
Enter fullscreen mode Exit fullscreen mode

Expected output:

   Enumerating objects: 5, done.
   Counting objects: 100% (5/5), done.
   Delta compression using up to 8 threads
   Compressing objects: 100% (3/3), done.
   Writing objects: 100% (3/3), 456 bytes | 456.00 KiB/s, done.
   Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
   To git.example.com:payments/service.git
    * [new branch]      payment-guard-2024-09-15 -> payment-guard-2024-09-15
Enter fullscreen mode Exit fullscreen mode
  1. If you discover a missing detail after push – create a follow‑up commit rather than amending a shared commit:
   git commit -am "docs(payment): add note about zero‑amount guard in README"
   git push
Enter fullscreen mode Exit fullscreen mode

The log now shows two logical units, preserving auditability.

  1. When rebasing to clean up history before merging – interactive rebase:
   git fetch origin
   git rebase -i origin/main
Enter fullscreen mode Exit fullscreen mode

In the editor, change pick to reword for the commit you want to improve, save, then edit the message as in step 2. Resolve any conflicts, then continue with git rebase --continue.

Confirmation: After rebase finishes, git log --oneline shows the updated subject line.

  1. Verify the final log – run:
   git log --format="%h %s" -n 3
Enter fullscreen mode Exit fullscreen mode

Expected output (example):

   9b2c3d4 fix(payment): guard against zero-amount invoices
   1a2b3c4 docs(payment): add note about zero-amount guard in README
   5e6f7g8 Merge branch 'main' into payment-guard-2024-09-15
Enter fullscreen mode Exit fullscreen mode

The concise subject now conveys intent at a glance.


When this breaks

Symptom Cause Fix
error: hook declined to update refs followed by Error: line 9 exceeds 72 characters commit-msg hook rejected a body line >72 chars. Re‑open the editor (git commit --amend) and wrap the offending line at 72 characters. Verify with git commit again.
fatal: cannot rebase: You have unstaged changes. Attempted git rebase -i while the working tree had modifications. Stash or commit the changes first: git stash push -m "temp" then retry the rebase. After rebase, restore with git stash pop.
CONFLICT (content): Merge conflict in src/payment/invoice.py during interactive rebase A rebase edit changed a line that later commits also modify. Resolve the conflict manually, git add src/payment/invoice.py, then git rebase --continue.
warning: commit message does not follow conventional format from CI lint step CI runs a linter (e.g., commitlint) that expects type(scope): subject. Amend the commit (git commit --amend) to match the pattern, e.g., fix(payment): …. Push with --force-with-lease if the branch is already remote and you have permission.
error: cannot lock ref 'refs/heads/main': File exists. after git push --force-with-lease Another developer pushed to main after you fetched. Re‑fetch: git fetch origin, re‑base your branch onto the updated origin/main, then push again.

Each failure surfaces a different discipline: respecting hooks, keeping a clean working tree, handling conflicts, adhering to team conventions, and coordinating with teammates.


What it costs

  • Time overhead – Writing a proper subject and body adds ~30 seconds per commit. In a large change set (e.g., 20 commits) that’s roughly 10 minutes, negligible compared to the hours saved when debugging ambiguous history.
  • Process friction – Enforcing hooks and templates may cause initial resistance; developers may view them as “red tape”. Mitigate by integrating the checks into the CI pipeline so failures appear early.
  • History size – Detailed bodies increase the size of each commit object modestly (a few hundred bytes). On repositories with millions of commits, the impact on storage is <0.1 %.
  • Learning curve – Junior engineers need guidance on the imperative style and scope naming. Pair‑programming or a short style guide helps amortise the cost.
  • Potential for accidental rewrites – Using --amend or rebase -i on shared branches can rewrite history, breaking downstream clones. The rule of thumb: never amend after push unless you have explicit permission and use --force-with-lease.

Overall, the cost is a small, predictable increase in developer effort that pays back in reduced on‑call time, faster code‑review turnaround, and a searchable commit log.


In the interview

Typical question

“Can you walk me through how you write a commit message and why you structure it that way?”

Weak answer – “I just write a short summary and push.”

Why it’s weak: It shows no awareness of the three‑part story, no mention of conventions, and no evidence of handling edge cases (e.g., large bodies, hooks, or shared history).

Strong answer – “I follow the Conventional Commits format: type(scope): subject. The subject is ≤50 chars, imperative, and describes what changed. I then add a body wrapped at 72 characters explaining why the change was needed and any how considerations, such as migration steps or performance impact. I keep a commit-msg hook that enforces the line‑length and pattern, and I use git commit --amend only on local commits before they are pushed. If a commit has already been shared, I add a follow‑up commit rather than rewriting history, to avoid breaking teammates’ clones. This discipline reduces the time spent triaging regressions because anyone can read the log and understand the intent without asking the author.”

What the interviewer probes further – “What trade‑offs did you consider when deciding to enforce a hook?”

A senior candidate should discuss the balance between developer friction and long‑term maintainability, citing metrics such as “we reduced on‑call tickets related to ambiguous commits by ~30 % after introducing the hook”.

Senior vs. junior expectations –

Junior: Explain the format and why a clear subject matters.

Senior: Discuss how the format integrates with CI, the impact on merge‑conflict resolution, and how to evolve the convention as the team grows.

Director/Engineering‑manager: Focus on the organisational benefit – faster incident post‑mortems, lower knowledge‑transfer cost, and measurable reduction in support tickets.

Follow‑up to separate theory from practice – “Tell me about a time you had to rewrite a commit message after it caused confusion in production. What steps did you take and what was the outcome?” A candidate who has done this will recount the exact git commands (git commit --amend, git push --force-with-lease) and the measurable improvement (e.g., “the next on‑call rotation spent 45 minutes less investigating the issue”).


Your tasks

  1. Write a clear commit

    Scenario: In a fresh clone of git@example.com:payments/service.git, create a branch feature/discount‑logic. Add a dummy file discount.py with a comment # TODO. Stage and commit using the Conventional Commits format, referencing ticket #5678.

    Done: git log -1 shows a subject ≤50 chars, a body wrapped at 72 chars, and a footer Closes #5678.

  2. Amend a local commit

    Scenario: The commit you just made mistakenly used feat instead of fix. Amend the commit to correct the type, without changing the diff.

    Done: git log -1 shows the corrected subject fix(discount): …. No new SHA appears in the remote (you haven’t pushed yet).

  3. Edit an older commit in a shared branch

    Scenario: You have pushed three commits to feature/discount‑logic. The second commit’s message is vague. Use interactive rebase to change its subject to refactor(discount): extract tax calculation. Resolve any conflicts.

    Done: After git push --force-with-lease, the remote history shows the updated subject, and git log displays the three commits in the new order.

  4. Trigger and fix the commit‑message hook

    Scenario: Add a body line longer than 72 characters to a new commit. The commit-msg hook should reject it. Then fix the line length and commit successfully.

    Done: The first git commit prints Error: line X exceeds 72 characters. After editing, the commit succeeds and the hook produces no output.

  5. Break and recover from a rebase conflict

    Scenario: While rebasing the branch onto origin/main, a conflict appears in discount.py. Resolve the conflict, stage the file, and continue the rebase.

    Done: git status shows no remaining conflicts, git rebase --continue finishes, and git log shows a linear history with your corrected messages.

Each task forces you to apply a concrete step from the chapter, and the “done” criteria let you verify success without needing an answer key.



Your tasks this week

Do the exercises above before the next chapter. Reading a tutorial and doing
one are different activities and only one of them changes what you can build.

Stuck on any of them? Say so — describe what you tried and what happened:
tell me where you got stuck. I read every one, and the questions
that come back more than twice get answered in the next chapter.

Engineering practice: git, code, tests

Chapter 1 of 12. New chapter every Sunday night.
Next: Branching, rebasing and conflicts without fear.

· The full syllabus and every chapter so far
· Subscribers also get the condensed notes for this chapter, the running
recap of everything the series has covered, and the extended guidance:
subscribe


Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. Portfolio · LinkedIn · GitHub.

Need this built, reviewed or taught to your team? Get in touch or email amit@devamit.co.in. Available for senior and founding engineering roles, consulting and training, remote worldwide.

Top comments (0)