AI EngineeringZero to ProductionHome·About·Contact
Developer Foundations · Chapter DF2

Git: track your work

Version control from your first commit to release automation: the three-area model, inspecting/undoing/recovering history, scripting Git in Python for changelogs + semver, and enforcing conventions.

⏱️ ~3.5 hours🧪 11 labs🎯 Beginner→Tech-lead
🌱 Start here — from zero Version control, from scratch — Git is how you never lose work, can always go back, and collaborate — from first commit to scripting it.

This chapter climbs from your first commit to reading Git from Python (release automation, repo analytics) and the conventions a tech lead sets. Shell runs in your terminal; the Python blocks build a throwaway repo so they run offline.

The words you'll hear (in plain terms):

TermWhat it actually means
repositorya project folder Git tracks.
commita saved snapshot with a message.
stagechoosing what goes in the next commit (git add).
HEADpointer to your current commit.
remotea hosted copy (GitHub).

What you need before starting:

  • The command line (DF1).
  • git --version to check it's installed.
  • A GitHub account for the remote parts.

New to the topic? Read this box, then take the chapters in order — each section is tagged essentialexpert so you always know the depth you're at.

Learning objectives

  • Use the working-dir → staging → repo model fluently.
  • Inspect history and undo mistakes at every stage.
  • Read Git from Python to automate changelogs/versioning.
  • Set and enforce the team's commit conventions.
▶ Runnable companionCode saved under code/df2-git-basics/. Python runs offline; configs are ready to use.

1 · The three areas essential

A change lives in one of three places, and Git commands move it between them: edit → git add (stage) → git commit (save to history).

Working dir your edits git add pick changes Staging ready to save git commit permanent snapshot
🗺️ How to read this diagram

This is the mental model everything else in the lesson builds on. Git tracks your work in three separate places, and each Git command just moves a change from one box to the next. Read the boxes left to right — that's the order a change travels in.

  • Working dir (leftmost box) is your project folder as it is right now — the files you edit in your editor. Git sees changes here but is not saving them yet.
  • The first arrow, labelled git add, means "pick which changes to include next". Running git add copies a change from the working dir into the middle box.
  • Staging (middle box) is a waiting room: the exact set of changes that will go into your next snapshot. Nothing is permanent yet — you can still add or remove things.
  • The second arrow, labelled git commit, saves everything in staging as one permanent snapshot in the rightmost box, with a message describing it.

In short: edit a file → git add it (working dir → staging) → git commit it (staging → history). Every Git workflow in this lesson is some version of that left-to-right trip.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
shell · init to first commit
init.shgit init
git config user.name "You"; git config user.email "you@example.com"
echo "# Project" > README.md
git status                        # README.md is untracked
git add README.md                 # stage it
git commit -m "Add README"
git log --oneline
▶ How this works

This is the complete birth of a repository: turn a plain folder into one Git watches, identify yourself, create a file, and make your first commit (your first saved snapshot). Run these lines top to bottom in a fresh folder.

  1. git init creates a hidden .git folder — that's what turns an ordinary directory into a Git repository. You only do this once per project.
  2. The two git config commands set the name and email Git stamps onto every commit, so history records who made each change.
  3. echo "# Project" > README.md creates a file (this is shell, not Git). Then git status shows it as untracked — Git can see the file but isn't following it yet.
  4. git add README.md stages it (working dir → staging), and git commit -m "Add README" saves that snapshot with a message. git log --oneline then lists your commits, one per line.

What the output means: git log --oneline prints a single line: a short commit ID (a sha) followed by your message Add README — proof the snapshot is saved in history.

Try this: Run git status again right after the commit. It now says "nothing to commit, working tree clean" — meaning all three areas match and there's nothing waiting to be saved.

2 · The everyday loop & good messages essential

shell · edit → stage → commit
loop.shecho "print('hi')" > app.py
git add app.py                    # or `git add -p` to stage hunks interactively
git commit -m "Add entrypoint"
git log --oneline --stat          # what changed in each commit
▶ How this works

This is the loop you'll repeat hundreds of times a day: edit → stage → commit. Once a repo exists (from the previous lab), real work is just running these three steps over and over as you make progress.

  1. echo "print('hi')" > app.py creates/edits a file — this is the edit step, happening in your working directory.
  2. git add app.py stages that change. The comment mentions git add -p: an interactive mode that lets you stage parts of a file (called "hunks") when one file has several unrelated edits.
  3. git commit -m "Add entrypoint" saves the staged change as a snapshot with a clear message.
  4. git log --oneline --stat shows history plus, for each commit, which files changed and by how many lines — a quick way to see the size of each change.

Try this: Edit app.py again but don't stage it, then run git status. You'll see it listed under "Changes not staged for commit" — the working dir and staging no longer match until you git add again.

Commit messages explain WHYGood: Fix off-by-one in pagination. Bad: stuff. Six months later, debugging with git blame, the message is the only context you'll have.

3 · Inspecting history intermediate

shell · read the past
inspect.shgit log --oneline --graph --all      # visual history
git show HEAD                         # the last commit's full diff
git diff                              # unstaged changes
git diff --staged                     # staged changes
git blame app.py                      # who last touched each line (and when/why)
▶ How this works

These are your read-only commands for understanding a repo's past — none of them change anything, so they're safe to run any time you're confused about what happened or who changed what.

  1. git log --oneline --graph --all draws history as a compact text diagram, including branches — a quick visual overview of how commits connect.
  2. git show HEAD displays the most recent commit in full, including its exact diff. HEAD is Git's word for "the commit you're currently on".
  3. git diff shows edits you've made but not yet staged; git diff --staged shows what's staged and ready to commit. Comparing the two tells you exactly which changes are in which area.
  4. git blame app.py annotates every line of a file with the commit, author, and date that last changed it — invaluable when you need to know why a line exists.

Try this: Run git diff, then git add the file, then git diff again. The second time it shows nothing — because the change moved to staging, and plain git diff only reports the working-dir-vs-staging gap.

4 · Undoing mistakes safely intermediate

SituationCommand
Discard un-staged editsgit restore file
Un-stage (keep edit)git restore --staged file
Fix last commit messagegit commit --amend
Undo last commit, keep workgit reset --soft HEAD~1
Recover a 'lost' commitgit reflog then git checkout <sha>
Almost nothing is truly lostEven after a bad reset, git reflog shows every state HEAD was at, so you can recover. Git's safety net is why you can experiment fearlessly.

5 · .gitignore — never commit junk or secrets advanced

config · a Python .gitignore
.gitignore.venv/
__pycache__/
*.pyc
.env                 # secrets/API keys — NEVER commit
.DS_Store
dist/
build/
*.log
▶ How this works

A .gitignore file is a plain list of things Git should pretend it can't see. Without it, you'd accidentally commit generated files, caches, and — most dangerously — secrets. Each line is one pattern to ignore.

  1. .venv/ and __pycache__/ (the trailing / means "a folder") skip your virtual environment and Python's compiled cache — big, machine-specific, and pointless to share.
  2. *.pyc uses * as a wildcard: ignore every file ending in .pyc. Same idea for *.log.
  3. .env is the important one — it holds secrets/API keys. The comment shouts NEVER commit it, because a committed secret is exposed forever (see the warning below the code).
  4. .DS_Store, dist/, and build/ are OS clutter and build output — regenerated automatically, so they don't belong in history.

Try this: Create a file called secret.env, add the line .env to .gitignore, then run git status. If the pattern matches, the file simply won't appear in the list of things to commit — Git is ignoring it.

A committed secret is compromised — rotate itHistory is permanent; deleting a key in a later commit doesn't erase it. .gitignore your .env BEFORE the first commit; if a secret ever lands, rotate it immediately (and consider history rewriting).

6 · Advanced — read Git from Python advanced

Release tooling and dashboards read Git programmatically. Shell out with subprocess (DF1) and parse. This builds a throwaway repo so it runs anywhere, error-free.

Python · generate a changelog from git log (runs)
changelog.pyimport subprocess, tempfile
from pathlib import Path

repo = Path(tempfile.mkdtemp())
def git(*a): return subprocess.run(["git", *a], cwd=repo, capture_output=True, text=True).stdout.strip()
git("init", "-q"); git("config","user.email","a@b.c"); git("config","user.name","D")
for msg in ["feat: login", "fix: null check", "feat: logout", "chore: bump deps"]:
    (repo/"f.txt").write_text(msg); git("add","."); git("commit","-q","-m",msg)

log = git("log", "--pretty=format:%s").splitlines()
groups = {}
for line in log:
    kind = line.split(":")[0]
    groups.setdefault(kind, []).append(line.split(": ",1)[1])
for kind in ("feat", "fix", "chore"):
    if groups.get(kind):
        print(f"## {kind}"); [print(" -", x) for x in groups[kind]]
## feat
 - login
 - logout
## fix
 - null check
## chore
 - bump deps
▶ How this works

This Python program answers a real release question — "what changed since last time?" — by reading Git from code instead of by hand. It builds a tiny throwaway repo, adds a few commits, then groups their messages into a changelog. It runs anywhere because it creates its own repo in a temp folder.

  1. repo = Path(tempfile.mkdtemp()) makes a brand-new empty folder, and the git(*a) helper runs any Git command inside that folder (cwd=repo) and hands back its text output. This is how Python "talks to" Git — by running the same commands you'd type.
  2. The for msg in [...] loop creates four commits, each with a conventional message like feat: login or fix: null check — a type, a colon, then a description.
  3. git("log", "--pretty=format:%s") asks Git for just the subject line of each commit. .split(":")[0] pulls the type (feat/fix/chore) and groups.setdefault(kind, []).append(...) files each message under its type.
  4. The final loop prints a section header (## feat) and a bullet per message — a finished, human-readable changelog.

What the output means: A grouped changelog: a ## feat section listing login and logout, a ## fix section with null check, and a ## chore section with bump deps — assembled entirely from the commit messages.

Try this: Add "docs: readme" to the list of messages. It won't appear in the output yet — because the last loop only prints feat, fix, and chore. Add "docs" to that tuple to include it.

7 · Professional — semantic versioning from commits professional

Conventional commits let you compute the next version automatically: a feat bumps minor, a fix bumps patch, a breaking change bumps major. This is how release automation works.

Python · auto-bump the version (runs)
semver.pydef next_version(current, commits):
    major, minor, patch = map(int, current.split("."))
    bump = "patch"
    for c in commits:
        if "BREAKING" in c: bump = "major"; break
        if c.startswith("feat") and bump != "major": bump = "minor"
        elif c.startswith("fix") and bump == "patch": bump = "patch"
    if bump == "major": return f"{major+1}.0.0"
    if bump == "minor": return f"{major}.{minor+1}.0"
    return f"{major}.{minor}.{patch+1}"

print(next_version("1.4.2", ["fix: typo"]))                    # 1.4.3
print(next_version("1.4.2", ["feat: new endpoint", "fix: x"])) # 1.5.0
print(next_version("1.4.2", ["feat: x", "BREAKING: drop v1"])) # 2.0.0
1.4.3
1.5.0
2.0.0
▶ How this works

This computes the next version number automatically from your commits. Versions look like MAJOR.MINOR.PATCH (e.g. 1.4.2): a bug-fix bumps the last number, a new feature bumps the middle, and a breaking change bumps the first and resets the rest. This is exactly how professional release tools decide the version.

  1. major, minor, patch = map(int, current.split(".")) splits "1.4.2" into the three numbers 1, 4, 2 so we can do math on them.
  2. bump starts at "patch" (the smallest change) and only ever gets bigger as the loop scans commits: a feat raises it to "minor", and the word BREAKING raises it all the way to "major" and stops early with break.
  3. The three if bump == ... lines at the end build the new version string: major resets minor and patch to 0 (2.0.0); minor resets only patch (1.5.0); otherwise just add one to patch (1.4.3).
  4. The three print(...) calls test it on sample commit lists, with the expected answer in the comment beside each.

What the output means: Three version numbers, one per test: 1.4.3 (only a fix), 1.5.0 (a feat present), and 2.0.0 (a BREAKING change wins over everything).

Try this: Call next_version("2.9.9", ["feat: x"]) and predict the answer before running: a feat bumps minor and resets patch, so you should get 2.10.0 (note 9 becomes 10, not carrying over — versions aren't decimals).

8 · Professional — connect to GitHub professional

shell · push to a remote
remote.shgit remote add origin git@github.com:you/proj.git
git branch -M main
git push -u origin main            # push + set upstream
git push                           # thereafter
git pull                           # get others' commits
git clone git@github.com:you/other.git
▶ How this works

So far everything lived only on your computer. These commands connect your local repo to a remote — a hosted copy on GitHub — so you can back it up and share it. A remote is just another copy of the same repo that lives on a server.

  1. git remote add origin git@github.com:you/proj.git tells your repo where the GitHub copy lives and nicknames it origin (the conventional name for "the main remote").
  2. git branch -M main renames your current branch to main — the standard name for the primary line of work.
  3. git push -u origin main uploads your commits and, thanks to -u, remembers the link so that afterwards a bare git push (upload) and git pull (download others' commits) just work with no extra arguments.
  4. git clone git@github.com:you/other.git is the reverse: it downloads a whole existing repo from GitHub onto your machine — how you start working on someone else's project.

Try this: Think of push and pull as sync: push sends your local commits up to GitHub, pull brings teammates' commits down. Nothing on the remote changes until you push.

9 · Tech-lead — enforce conventions with a hook tech-lead

A lead makes history useful and automatable by mandating a commit convention and enforcing it with a hook (and in CI). Then changelogs (§6) and versioning (§7) come for free.

Python · a commit-msg hook (runs)
commit_msg_hook.pyimport re
PATTERN = re.compile(r"^(feat|fix|docs|refactor|test|chore)(\(.+\))?: .{1,72}$")

def valid(message: str) -> bool:
    first = message.strip().splitlines()[0] if message.strip() else ""
    return bool(PATTERN.match(first))

for msg in ["feat: add login", "fixed stuff", "fix(auth): handle null token", "wip"]:
    print(f"{'OK    ' if valid(msg) else 'REJECT'}  {msg}")
# real hook (.git/hooks/commit-msg):  sys.exit(0 if valid(open(sys.argv[1]).read()) else 1)
OK      feat: add login
REJECT  fixed stuff
OK      fix(auth): handle null token
REJECT  wip
▶ How this works

A hook is a script Git runs automatically at a certain moment — here, right after you write a commit message. This one checks the message follows the team's convention and rejects the commit if it doesn't, so bad messages never reach history in the first place.

  1. PATTERN = re.compile(r"^(feat|fix|docs|refactor|test|chore)(\(.+\))?: .{1,72}$") is a regular expression — a pattern for text. In plain English it demands: one of those approved types, an optional (scope), then : and a description of 1–72 characters.
  2. valid(message) takes the first line of the message (.splitlines()[0]) and returns True only if it matches the pattern — bool(PATTERN.match(first)).
  3. The for loop feeds four sample messages through valid and prints OK or REJECT for each, so you can see the rule in action.
  4. The final comment shows the real hook: in an actual .git/hooks/commit-msg file it would sys.exit(0) to allow the commit or sys.exit(1) to block it — a non-zero exit is how a hook says "no".

What the output means: Four lines: feat: add login and fix(auth): handle null token pass (OK), while fixed stuff and wip fail (REJECT) because they don't start with an approved type and colon.

Try this: Add "docs: update readme" to the list — it passes. Then try "feat add login" (no colon) — it's rejected, because the pattern requires the : separator.

Conventions unlock automationOnce commits follow a pattern, you get automatic changelogs, semantic version bumps, and searchable history for free. The tech-lead move: pick the convention, enforce it with a hook + CI, and reap the automation across §6–§7.

Exercise DF2.1 — Version + automate a repo

Context: Put the whole chapter together on one repo: conventions in, automation out, plus the recovery move that lets you experiment without fear. This is the daily rhythm of a well-run project.

Your task: Init a repo with a proper .gitignore, make conventional commits, then use the changelog and semver logic to generate release notes and the next version — add the commit-msg hook, and deliberately reset then recover a commit.

Requirements:

  • Start from a .gitignore that keeps env/caches/secrets out
  • Make several conventional commits (feat:, fix:, chore:…)
  • Generate grouped release notes and compute the next version from those commits
  • Install the commit-msg hook and confirm it rejects a non-conforming message
  • Drop a commit with git reset, then recover it via git reflog + checkout

💡 Hint: The reflog records every state HEAD was ever at, so a "lost" commit after a bad reset is almost always still recoverable — that safety net is the point of the exercise.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Init, stage, commitBeginner

Context: Every project's history starts the same way: turn a plain folder into a repo and make a first snapshot. This is the working-tree → staging → history trip that every later Git workflow is built on.

Your task: Create a new repo in an empty folder, add a file hello.txt, and make your first commit with a clear message — showing the change travel through all three areas.

Requirements:

  • Initialize the repo with git init
  • Create the file, then confirm it shows as untracked with git status
  • Stage it with git add hello.txt (working tree → staging)
  • Commit with git commit -m and a message that says what and why
  • Verify the snapshot landed with git log --oneline

💡 Hint: Write the message in the imperative mood describing the change — not "stuff" or "wip" — because that line is your only context six months later.

Show solution

The everyday loop is statusaddcommit:

git init
echo "hello" > hello.txt
git status            # hello.txt is untracked (working tree)
git add hello.txt     # now staged
git commit -m "Add hello.txt with a greeting"
git log --oneline     # one commit in history

The message says what and why in the imperative mood, not "stuff" or "wip".

Exercise 2 · Inspect what changedIntermediate

Context: In real work you often touch several files but want each commit to be one focused change. Reading the diffs and staging selectively is how you keep history reviewable.

Your task: You edited two files but only want to commit one. Show the diff of unstaged changes, stage a single file, confirm exactly what is staged, then commit only that file.

Requirements:

  • View unstaged edits with git diff (working tree vs staging)
  • Stage just the intended file with git add <file>
  • Confirm what will be committed with git diff --staged
  • Use git status to see one file staged and the other still modified
  • Commit only the staged file with a focused message

💡 Hint: Remember the two diffs answer different questions: plain git diff is working-vs-staged, git diff --staged is staged-vs-last-commit.

Show solution
git diff                 # unstaged changes across all files
git add report.py        # stage just this one
git diff --staged        # exactly what will go into the commit
git status               # report.py staged, notes.txt still modified
git commit -m "Fix off-by-one in report totals"

git diff = working vs staged; git diff --staged = staged vs last commit. Committing a focused change keeps history reviewable.

Exercise 3 · Undo mistakes safelyAdvanced

Context: Git's safety net is why you can experiment fearlessly: almost every mistake before you push is reversible. Knowing the three everyday undos means you never fear breaking your local repo.

Your task: Practise three reversible undos — unstage a file added by accident, discard uncommitted edits to a file, and reword the commit you just made — and explain why none of them rewrite shared history.

Requirements:

  • Un-stage but keep the edit with git restore --staged <file>
  • Discard local edits to a tracked file with git restore <file>
  • Reword the most recent unpushed commit with git commit --amend
  • Explain that all three touch only local state
  • Note that a shared commit should be undone with git revert <sha>, not reset

💡 Hint: The dividing line is whether a commit has been pushed: rewriting local history is safe, rewriting history teammates already pulled is not.

Show solution
# 1. unstage but keep the edit in the working tree
git restore --staged secrets.env

# 2. throw away local edits to a tracked file (irreversible for that edit)
git restore config.py

# 3. reword the most recent (unpushed) commit
git commit --amend -m "Add rate limiter to the API client"

All three touch only local state. Prefer git revert <sha> (a new inverse commit) over reset once a commit is shared, because rewriting pushed history breaks teammates' clones.

Exercise 4 · A .gitignore that keeps secrets outExpert

Context: A committed secret is compromised forever — history is permanent, so deleting a key in a later commit doesn't erase it. A correct .gitignore is the first line of defence, and handling an already-tracked secret is the harder real case.

Your task: Write a .gitignore for a Python project that excludes the virtualenv, byte-code, and a .env secrets file — then handle a secret that was already committed.

Requirements:

  • Ignore the environment and caches (.venv/, __pycache__/, *.pyc)
  • Ignore the secrets file .env and any *.key files
  • Use trailing / for folders and * as a wildcard
  • Stop tracking an already-committed secret with git rm --cached .env (file stays on disk)
  • State that a secret already in history must be rotated and scrubbed (filter-repo / BFG)

💡 Hint: Ignoring only prevents future tracking — if the key is already in a past commit, assume it is compromised and rotate it regardless.

Show solution
# .gitignore
__pycache__/
*.pyc
.venv/
.env
*.key
.DS_Store

Ignoring only prevents future tracking. If .env is already tracked, stop tracking it (file stays on disk):

git rm --cached .env
git commit -m "Stop tracking .env; add to .gitignore"

If the secret is in history, ignoring is not enough — rotate the credential and scrub history with git filter-repo (or BFG). Assume any pushed secret is compromised.

Exercise 5 · Derive the next version from commitsProfessional

Context: Conventional commit messages are machine-readable, so the next version number can be computed instead of guessed. This is the engine inside tools like semantic-release.

Your task: Read the repo's tags and commit subjects from Python and compute the next semantic version: a feat: since the last tag bumps minor, fix:/other bumps patch, and a BREAKING note bumps major.

Requirements:

  • Shell out to git (via subprocess) to read the last tag and commits since it
  • Parse MAJOR.MINOR.PATCH out of the tag, defaulting to 0.0.0 when none exists
  • Scan commit subjects: BREAKING → major, feat → minor, else patch
  • A bigger bump wins and a major reset sends minor and patch to 0
  • Print the current tag and the computed next version

💡 Hint: Let the bump level only ever escalate as you scan — start at patch, raise to minor on a feat, and jump straight to major (breaking out) the moment you see BREAKING.

Show solution

Runnable — shells out to git, works in any repo with at least one commit:

import subprocess, re

def sh(*args):
    return subprocess.run(["git", *args], capture_output=True,
                          text=True).stdout.strip()

def last_tag():
    t = sh("describe", "--tags", "--abbrev=0")
    return t or "v0.0.0"

def commits_since(tag):
    rng = f"{tag}..HEAD" if tag != "v0.0.0" else "HEAD"
    log = sh("log", rng, "--format=%s%n%b")
    return log.splitlines()

def next_version():
    tag = last_tag()
    major, minor, patch = map(int, re.findall(r"\d+", tag)[:3] or (0, 0, 0))
    lines = commits_since(tag)
    bump = "patch"
    for ln in lines:
        if "BREAKING" in ln:
            bump = "major"; break
        if ln.startswith("feat") and bump != "major":
            bump = "minor"
    if bump == "major":  major, minor, patch = major + 1, 0, 0
    elif bump == "minor": minor, patch = minor + 1, 0
    else:                 patch += 1
    return f"v{major}.{minor}.{patch}"

print("current:", last_tag(), "-> next:", next_version())

This is the core of tools like semantic-release: the commit convention becomes machine-readable release automation.

Exercise 6 · Enforce the convention with a hookIndustry scenario

Context: New engineers keep writing messages like "fix" or "update", which quietly breaks every downstream automation. Enforcing the convention at commit time — not in review — is what actually keeps history clean.

Your task: Add a commit-msg hook that rejects any message not matching Conventional Commits, so the rule is enforced automatically before a bad message reaches history.

Requirements:

  • Match the first line against a regex of approved types (feat|fix|docs|refactor|test|chore…) with optional (scope) and a : description
  • Read the message from the file path Git passes as sys.argv[1]
  • Print a helpful example and sys.exit(1) to abort when it doesn't match
  • Install it at .git/hooks/commit-msg and make it executable (chmod +x)
  • Note that .git/hooks isn't versioned, so share it via a tracked hooks/ dir + core.hooksPath (or pre-commit)

💡 Hint: A non-zero exit is how a hook says "no" — return 0 to allow the commit and 1 to block it, and keep the pattern anchored to the first line only.

Show solution

Save as .git/hooks/commit-msg and chmod +x it. The hook gets the message file path as $1:

#!/usr/bin/env python3
import sys, re

PATTERN = re.compile(
    r"^(feat|fix|docs|refactor|test|chore|perf)(\(.+\))?: .{1,}")

msg = open(sys.argv[1]).readline().rstrip("\n")
if not PATTERN.match(msg):
    print("commit rejected: message must be Conventional Commits, e.g.")
    print("  feat(auth): add token refresh")
    print(f"got: {msg!r}")
    sys.exit(1)

A non-zero exit aborts the commit. To share it with the team, commit the script under a tracked hooks/ dir and wire it with git config core.hooksPath hooks (or use pre-commit), since .git/hooks itself is not versioned.

✓ Checkpoint — you can move on when you can…

  • Run the repo→stage→commit loop with good messages.
  • Inspect history and undo/recover at every stage.
  • Read Git from Python for changelogs + version bumps.
  • Enforce team commit conventions with a hook.

Knowledge check check yourself

✓ Knowledge check

What are Git's three areas, and which command moves a change between each pair?

Show answer
Working directory (your edits) → git add → Staging (changes chosen for the next snapshot) → git commit → the repository/history (a permanent snapshot).
✓ Knowledge check

Under conventional commits, how do a fix, a feat, and a breaking change each affect the next semantic version?

Show answer
A fix bumps the patch number, a feat bumps the minor (resetting patch to 0), and a breaking change bumps the major (resetting minor and patch to 0) — e.g. 1.4.2 → 1.4.3, 1.5.0, or 2.0.0 respectively.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in