devopsdiary
Next chapter

diary / chapters / 04

chapter 04 · ~40 min · terminal lab

Git, branches and working with humans

Git is a save-game system for code: every commit is a checkpoint you can return to. Most people learn four commands and then live in fear of the rest. This chapter gives you the mental model, so the commands stop being magic spells.

TOPIC 01The mental model

A Git repository is a chain of snapshots. Each snapshot (a commit) records the full state of the project, who made it, when, why, and which commit came before it. That "came before" link is what makes the chain — and what makes history a graph you can walk.

Three facts do most of the explaining:

  • A commit is immutable and identified by its hash (a91f3c…). Change anything about it and you have a different commit with a different hash.
  • A branch is just a movable label pointing at one commit. Creating a branch costs nothing — it writes 41 bytes. This is why Git encourages branching where older tools punished it.
  • HEAD means "where I am standing right now", normally the tip of your current branch.
real life

Think of a shared kitchen notebook where every recipe change is written on a new page, and no page is ever erased. A branch is a sticky tab clipped to a page — "this is the version we're testing". Moving the tab is instant and harmless; tearing pages out is what causes arguments. Git's whole design is: add pages, move tabs, never erase.

TOPIC 02The three areas

The single biggest source of Git confusion is not knowing which of these three places your change is currently in.

where your change lives
  working directory        staging area (index)        repository
  ─────────────────        ────────────────────        ──────────
  you edit files    ──▸    git add    ──▸   git commit    ──▸  history
  "the countertop"         "the tray"                    "the notebook"

  git status      → what is where, right now
  git diff        → countertop vs tray  (unstaged changes)
  git diff --staged → tray vs notebook  (what will be committed)
  git restore f   → throw away countertop changes to f
  git restore --staged f → take f off the tray, keep the edits
real life

Packing a parcel. The working directory is your desk covered in things. The staging area is the box you are choosing what to put in. The commit is sealing and labelling it. Staging exists so that one messy afternoon can be committed as three clean, separately-labelled parcels — which is exactly what makes a rollback surgical later.

terminal · the daily loop
git status                       # run this constantly. it is free.
git add src/cache.js             # stage one file
git add -p                       # stage selected chunks — great for tidy commits
git commit -m "add redis cache for product lookups"
git log --oneline --graph -10    # see the shape of recent history
git push origin feat/cache

TOPIC 03Commits worth reading

Commit messages are documentation written at the only moment you fully understand the change. Six months later, during an incident, someone will run git log on the file that broke and read yours. Write for that person.

a good commit message
fix(checkout): stop double-charging on retry

Stripe retries a request when our response takes over 10s. We had no
idempotency key, so the retry created a second charge.

Adds an idempotency key derived from the order id, and lowers our
own timeout to 8s so we fail before Stripe retries.

Fixes #4182

Conventional commits — feat:, fix:, chore:, docs:, refactor:, test:, ci: — are worth adopting because tools can read them: version numbers and changelogs can be generated automatically from the prefixes (chapter 09). Rules of thumb: subject in the imperative and under ~60 characters; body explains why, not what (the diff already says what); one logical change per commit.

TOPIC 04Branches are just labels

terminal · branching
git switch -c feat/cache          # create + switch (old form: checkout -b)
git switch main                   # move between branches
git branch -a                     # list local + remote branches
git branch -d feat/cache          # delete merged branch (-D forces)

git fetch origin                  # download refs, change nothing locally
git pull                          # fetch + merge into current branch
git pull --rebase                 # fetch + replay your commits on top (tidier)
fetch vs pull

fetch is reading the news; pull is reading it and immediately rearranging your desk to match. When you are unsure of the state of things — mid-conflict, mid-rebase, mid-panic — fetch then git log --oneline origin/main..HEAD shows you exactly what you have that the remote does not.

TOPIC 05Merge vs rebase

Both answer "my branch is behind main, what now?" — differently.

the same situation, two outcomes
# starting point: main moved on while you worked
main    A───B───C───D
                 \
feat              E───F        (your two commits)

# git merge main → a merge commit joins the histories
main    A───B───C───D───────M
                 \         /
feat              E───F───┘     history is true but braided

# git rebase main → your commits are recreated on top of D
main    A───B───C───D
                     \
feat                  E'──F'    linear, but E' and F' are NEW commits
MergeRebase
HistoryPreserves exactly what happenedRewritten to look linear
HashesUnchangedNew hashes for your commits
Safe on shared branchesYesNo — it rewrites what others may have pulled
Best forBringing a finished branch into mainTidying your own branch before review
real life

Merging is stapling your pages into the notebook with a note saying "these were written in parallel". Rebasing is copying your pages out in fresh handwriting after everyone else's, then throwing the originals away. The copy reads beautifully — but if a colleague had already photocopied your original pages, their copy and yours no longer match. Hence the one rule: never rebase a branch other people are working on.

The practical convention most teams land on: rebase your own feature branch onto main while you work (keeps review clean), then merge — often squashed — into main via a pull request.

TOPIC 06Conflicts, calmly

A conflict means two commits changed the same lines and Git will not guess. It is not an error; it is a question.

what a conflict looks like in the file
<<<<<<< HEAD                 ← what is on your current branch
  const TIMEOUT = 5000;
=======
  const TIMEOUT = 8000;        ← what is coming in
>>>>>>> main
terminal · resolving
git status                    # lists exactly which files conflict
# edit each file: keep one side, the other, or write the correct third thing.
# delete all the <<<, ===, >>> markers.
git add src/config.js        # "resolved"
git commit                   # finishes a merge
git rebase --continue        # finishes a rebase step

git merge --abort            # escape hatch: put everything back
git rebase --abort

Two habits make conflicts rare: small, short-lived branches (a branch open for two hours cannot conflict much), and syncing with main daily instead of once at the end.

TOPIC 07Pull requests and review

A pull request is a proposal: "here is a branch, please review and merge it." For DevOps it is the control point — it is where CI runs, where scans report, and where the human conversation is recorded.

What a reviewable pull request looks like:

  • Small. Under ~400 changed lines gets a real review; 4,000 gets a rubber stamp.
  • One purpose. A refactor and a bug fix in one PR means neither can be reverted alone.
  • A description that answers three questions: what changes, why, and how you know it works.
  • Green checks. Never ask a human to review something the robots have not approved (chapter 05).

As a reviewer, ask about behaviour rather than style — style belongs to a formatter. Useful questions: What happens when this fails? Is anything here irreversible? How would we notice if this broke in production? Are secrets or credentials involved?

branch protection — the settings that matter

On main: require a pull request, require CI to pass, require at least one approval, forbid force-pushes, and prefer linear history. Five checkboxes that eliminate the entire category of "someone pushed straight to main on a Friday".

TOPIC 08Branching strategies

Trunk-based development recommended
One long-lived branch (main), always releasable. Short branches (hours to two days) merged via PR. Unfinished work hides behind feature flags, not behind branches. This is what high-performing teams do, and what pairs naturally with CI/CD.
GitHub flow
Trunk-based plus "deploy every merge to main". Simple and effective for web services.
Git flow
develop, release/*, hotfix/*, feature/*. Designed for versioned software with scheduled releases and multiple supported versions. Heavy for a web app deploying daily — you will meet it in older codebases, and it is worth knowing why it exists.
Environment branches (dev → staging → prod)
Common and tempting; leads to cherry-picking, drift and "which branch is production?" confusion. Better: one artifact promoted through environments (chapter 09).

TOPIC 09Tags, versions and releases

A tag is a permanent name for one commit — that is how you mark "this exact code is v2.3.0" and how a rollback knows what to go back to.

terminal · tagging a release
git tag -a v2.3.0 -m "checkout retry fix"
git push origin v2.3.0
git describe --tags          # v2.3.0-4-ga91f3c = 4 commits past v2.3.0
git tag --sort=-creatordate | head -5

Semantic versioning — MAJOR.MINOR.PATCH: MAJOR for a breaking change, MINOR for a backwards-compatible feature, PATCH for a fix. The reason to care is dependency ranges: ^1.4.0 accepts 1.5.0 but not 2.0.0, so the discipline of others' version numbers directly determines whether your build breaks overnight.

For internal services, many teams skip semver entirely and tag images with the commit SHA — notes-api:a91f3c. It is unambiguous, automatic, and links a running container straight back to a line of code.

TOPIC 10Undoing almost anything

terminal · the recovery table
# I changed a file and want the committed version back
git restore src/app.js

# I staged something by mistake (keep the edit)
git restore --staged src/app.js

# my last commit message is wrong / I forgot a file (NOT yet pushed)
git commit --amend

# undo the last commit, keep changes staged
git reset --soft HEAD~1

# undo the last commit and DISCARD the changes (dangerous)
git reset --hard HEAD~1

# the commit is already pushed: add a new commit that reverses it
git revert a91f3c            # the only safe undo for shared history

# park work in progress and come back to it
git stash push -m "half-done cache"
git stash list && git stash pop

# I did something terrible and lost commits
git reflog                   # every position HEAD has held, for ~90 days
git reset --hard HEAD@{4}    # travel back to before the mistake

# which commit introduced this bug? (binary search, brilliant and underused)
git bisect start && git bisect bad && git bisect good v2.2.0
reset vs revert — get this right once

reset rewrites your local history: fine for commits nobody has seen. revert makes a new commit that undoes an old one: the correct tool for anything already pushed, because it changes nothing anyone else has. And git reflog is the reason almost nothing in Git is truly lost — before you panic, look there.

TOPIC 11Repo hygiene for DevOps

  • A real .gitignore. Never commit node_modules/, .env, *.pem, .terraform/, *.tfstate, build output or IDE folders.
  • Never commit secrets. If you do, and push: assume it is compromised, rotate the credential first, then clean history. Removing the commit is not containment — the credential is what matters (chapter 11).
  • Commit a .env.example with keys and dummy values, so a new joiner knows what configuration exists without learning any real values.
  • Keep infrastructure code in Git too. Terraform, Kubernetes manifests, pipeline definitions and runbooks all belong under review like any other code — that is the whole premise of chapters 08 and 09.
  • Pre-commit hooks catch problems before they leave your laptop: formatter, linter, secret scanner. Keep them fast (under a couple of seconds) or people will bypass them.
terminal · one-time global setup
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global pull.rebase true      # tidier pulls
git config --global alias.lg "log --oneline --graph --decorate -20"
remember this much
  • Commits are immutable snapshots; branches are cheap movable labels; HEAD is where you stand.
  • Working directory → staging area → repository. git status tells you which one your change is in.
  • Rebase your own branch; merge shared ones. Never rebase what others have pulled.
  • reset for private history, revert for anything pushed, reflog when you think you lost work.
  • Short-lived branches + small PRs + branch protection is the combination that makes daily deploys possible.

LABThe whole loop, including the scary parts

40 minutes · a throwaway GitHub repo

Branch, review, conflict, revert, recover

  1. Create an empty repo on GitHub, clone it, and commit a small app.js and a README.md on main. Push.
  2. Turn on branch protection for main: require a pull request and forbid force-pushes.
  3. Create feat/timeout, change a constant, commit with a conventional message, push, and open a pull request. Write a description that answers what/why/how-verified. Merge it.
  4. Manufacture a conflict: on main, change that same line to a third value and commit. On a new branch from the old main, change it to yet another value. Open a PR and watch GitHub report the conflict. Resolve it locally with git rebase main, read the markers, fix, git add, git rebase --continue, force-push your branch (this one is yours, so it is safe).
  5. Practise a rollback: merge something deliberately wrong, then git revert <sha> on main and push. Note that history now shows both the mistake and its reversal — that is a feature, not a mess.
  6. Practise recovery: on a scratch branch, run git reset --hard HEAD~2 and "lose" two commits. Get them back with git reflog and git reset --hard HEAD@{1}. Do this once now, while it is not an emergency.
  7. Stretch: add a pre-commit hook that refuses a commit containing the string AKIA (the prefix of an AWS access key). Test it by trying to commit one.

The point of step 6: everyone eventually runs a destructive Git command by accident. Having recovered on purpose once means you will remember reflog exists on the day it matters.

CHECKCheck yourself

You pushed a broken commit to main an hour ago. Four colleagues have already pulled it. What is the correct fix?

Once history is shared, rewriting it breaks everyone else's copy — their next pull produces a mess, and any branch based on the old commit diverges. revert adds a new commit that undoes the change, leaving shared history intact. Reset, amend and force-push are for commits that have never left your machine.

Why is a two-week-old feature branch a DevOps problem, not just a Git problem?

A long branch is a large batch by another name. Its code has never been integrated with everyone else's, so conflicts pile up and the first real test of the combination happens at merge time — exactly the "big risky release" pattern chapter 01 warned about. Trunk-based development plus feature flags keeps batches small without hiding work in branches.

You committed .env containing a live database password and pushed. What is the first thing to do?

The moment a secret is pushed it may have been cloned, cached by a mirror, or scraped by a bot within seconds. Rotation is the only action that actually removes the risk. Cleaning history, updating .gitignore and adding a secret scanner are all correct follow-ups — they just are not containment.

saved in this browser only — no account needed