Module 12 — Tags and Releases (git tag, describe, semver)

Updated 8 September 2026

Module 12 — Tags and Releases (git tag, describe, semver). A commit hash is precise but unmemorable; "v2.4.0" is what humans, deploys, and changelogs actually reference. Tags are Git's permanent, human-readable names for the commits that matter — releases. This module covers lightweight vs annotated tags, semantic versioning, git describe, pushing tags (they don't travel automatically — a classic surprise), and the signed tags that make a release cryptographically provable.

🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

Before you start. You need Module 3 (objects and refs — a tag is one more object type and one more ref), Module 8 (pushing — tags push separately), and ideally Module 1's note that commit identity is unverified (signing is the fix, Part D). A fresh kitchen repo works; the exercises build what they need. Part D's signed tags require a signing key configured — the exercise says how, and is optional if you have none.

Part A — Two kinds of tag

A1. Lightweight and annotated

A tag is a name pinned to a specific commit — and unlike a branch (Module 5's moving pointer), a tag never moves. It marks one commit forever: "this exact snapshot is v1.0.0." Git has two kinds. A lightweight tag is just a ref — a file under .git/refs/tags/ holding a commit hash, nothing more (Module 3's ref, pointed at a commit instead of a branch). An annotated tag is a real object in the database — Git's fourth object type (Module 3 named it and deferred it to here): it stores the tagger's name, email, date, a message, and (Part D) an optional signature, then points at the commit. git tag <name> makes a lightweight tag; git tag -a <name> -m "message" makes an annotated one. For releases, use annotated — the metadata (who cut it, when, why) is exactly what a release needs, and only annotated tags can be signed.

🧪 Exercise 12.1 — make one of each, then look inside
bash
cd ~/git-course/kitchen              # a repo with a few commits
git tag v0.9-beta                                 # lightweight
git tag -a v1.0.0 -m "First stable release: core bread recipes"   # annotated
git tag                                           # list them
git cat-file -t v0.9-beta            # what kind of object does each name?
git cat-file -t v1.0.0
git cat-file -p v1.0.0               # read the annotated tag object
Expected result — click to reveal
plain text
v0.9-beta
v1.0.0
commit
tag
object 5de043ae19dde738a1c05691b66ecbd7bb7a3cb7
type commit
tag v1.0.0
tagger Aisha Rahman <[email protected]> 1788819385 +0000

First stable release: core bread recipes

What to read out of it: git cat-file -t reveals the difference at the object level — the lightweight tag v0.9-beta resolves straight to a commit (it's a bare pointer, no object of its own), while v1.0.0 is a tag object. Reading that tag object shows its anatomy: it contains the commit hash (object …), the target's type, its own name, the tagger line (identity + timestamp — the metadata a lightweight tag lacks), and the message. This is Module 3's fourth object type in the flesh: blob, tree, commit, and now tag — all content-addressed, all read with the same cat-file.

Real-world analogy — the museum plaque vs a sticky dot

A lightweight tag is a sticky dot on a painting: "this one." Useful, but it tells you nothing — who placed it, when, why this painting. An annotated tag is the engraved plaque beside the artwork: title, date acquired, curator's note, and the museum's seal (the signature). Both point at the same painting; only the plaque carries the story of the designation.

Where the analogy stops working. A plaque can be re-engraved and a painting reframed; the record drifts. A Git tag is anchored by content-addressing — the annotated tag object names the commit by hash, so it is bound to that exact snapshot and provably so. And a museum's seal can be forged onto a plaque; a signed tag's cryptographic signature (Part D) cannot — verification recomputes it against a public key. The plaque is a description; the signed tag is evidence.

A2. Tagging, listing, deleting

Official docs: git-tag manual

Tags are not limited to "right now." git tag -a <name> <commit> tags any historical commit — the common "we forgot to tag last week's release" fix. Listing scales: git tag -l "v1.*" filters by pattern, -n1 shows each tag's message, and --sort=-v:refname sorts by version number (not alphabetically — critical, because plain sort puts v1.10.0 before v1.9.0). Deleting is git tag -d <name> — but note this deletes only your local tag; a pushed tag needs separate remote deletion (Part C).

🧪 Exercise 12.2 — tag the past, filter, delete
bash
cd ~/git-course/kitchen
FIRST=$(git rev-list --max-parents=0 HEAD)   # the root commit's hash
git tag -a v0.1.0 -m "Initial prototype" $FIRST
git tag -l "v0.*" -n1                 # matching tags, with messages
git tag --sort=-v:refname            # version-sorted, newest first
git tag -d v0.9-beta                 # remove the lightweight beta locally
git tag
Expected result — click to reveal
plain text
v0.1.0          Initial prototype
v0.9-beta       Add baking
v1.0.0
v0.9-beta
v0.1.0
Deleted tag 'v0.9-beta' (was 5de043a)
v0.1.0
v1.0.0

What to read out of it: v0.1.0 now marks the first commit though you created the tag just now — tags name any commit, past or present. The -n1 listing shows annotated tags' messages (Initial prototype) and, for the lightweight v0.9-beta, falls back to the commit's subject (Add baking) since it has no message of its own — a visible symptom of the lightweight/annotated difference. --sort=-v:refname ordered them as real versions (v1.0.0 > v0.9-beta > v0.1.0). The delete printed the hash it removed (was 5de043a) — the same last-chance note branch -d gives (Module 5), because deleting a tag is deleting a ref.

🎯 Interview questions — Part A

🎯 "What is the difference between a lightweight tag and an annotated tag?" — asked verbatim in GeeksforGeeks' 70+ Git Interview Questions, updated Jul 2026

A lightweight tag is just a ref — a name pointing straight at a commit, with no object of its own and no metadata; effectively a private bookmark. An annotated tag is a full Git object storing the tagger's name and email, a timestamp, a message, and optionally a GPG/SSH signature, which then points at the commit. Consequences: annotated tags record who tagged, when, and why; they can be signed and verified; and git describe prefers them. Create lightweight with git tag <name>, annotated with git tag -a <name> -m "…". Use annotated for anything shared — releases especially.

The details that separate candidates: an average answer says "annotated has a message." A strong answer states that an annotated tag is a distinct object (Git's fourth type — blob/tree/commit/tag) verifiable with git cat-file -t, that only annotated tags can be signed, and that git describe and most release tooling assume annotated. The practical rule they want: lightweight for throwaway local markers, annotated for every release — because a release needs provenance, and a bare ref carries none.

🎯 "What is the purpose of 'git tag -a'?" — asked verbatim in GeeksforGeeks (updated Jul 2026) and Interview Coder, Sep 2025

git tag -a <name> creates an annotated tag: a stored object carrying the tagger's identity, date, and a message (supplied with -m or via the editor), pointing at a commit (HEAD by default, or a given hash). Its purpose is durable, attributable release marking — the metadata answers "who cut this release, when, and what is it," which a lightweight tag cannot. Add -s to sign it cryptographically for provable authenticity. It's the standard way to mark versions: git tag -a v2.1.0 -m "Release 2.1.0", then push it.

The details that separate candidates: an average answer says "makes a tag with a message." A strong answer positions -a as the release tool (metadata + signability + describe support), can tag a past commit (git tag -a v2.1.0 <hash> when a release went out untagged), and knows the immediate follow-up most people forget — tags don't push with commits; git push origin <tag> or --tags is required (Part C). Mentioning -s/-m and that annotated is a real object rounds it out.

Part B — Semantic versioning and git describe

B1. What the numbers mean

Tag names are free-form, but a convention won: semantic versioning (semver), MAJOR.MINOR.PATCH. The rules encode a promise to your users: bump PATCH (1.4.2 → 1.4.3) for backward-compatible bug fixes; MINOR (1.4.2 → 1.5.0) for backward-compatible new features; MAJOR (1.4.2 → 2.0.0) for a breaking change that requires consumers to adapt. Optional suffixes mark pre-releases: 2.0.0-rc.1, 1.0.0-beta. The payoff is that a version number becomes machine-readable intent: dependency tools can auto-accept patch and minor updates while pinning majors, precisely because the numbers carry a compatibility contract — not decoration. (Git conventionally prefixes the tag with v: v2.0.0; the v is habit, semver's spec is the bare number.)

Trap: semver is a promise, and the promise is only as good as your discipline. The classic breach: shipping a breaking change as a MINOR or PATCH bump because "it's a small change." Small ≠ compatible — a one-character change to a default value can break every consumer, and if it went out as a patch, their auto-updates just broke in production. The version number is an API contract with everyone who depends on you; bump MAJOR whenever behavior consumers rely on changes, regardless of diff size. Teams that treat semver casually teach their users to pin exact versions and never auto-update — which defeats the entire point.

B2. git describe — where am I relative to the last release?

Official docs: git-describe manual

git describe answers "what version is this build?" for any commit — including the commits between releases. It finds the most recent tag reachable from a commit and, if you are past it, appends how far: v1.0.0-2-gd255463 means "2 commits after tag v1.0.0, at commit d255463" (the g prefix is for "git"). Exactly on a tagged commit, it prints just the tag. This is how build systems stamp version strings automatically — every CI artifact gets a name that pinpoints its source commit relative to the last release, with no manual bookkeeping. Use --tags to consider lightweight tags too (by default describe uses only annotated ones — another reason releases should be annotated).

🧪 Exercise 12.3 — describe a release, and the commits after it
bash
cd ~/git-course/kitchen              # v1.0.0 is on the current tip from 12.1
echo "more" >> bread.txt && git commit -qa -m "Tweak bread"
echo "more2" >> bread.txt && git commit -qa -m "Tweak bread again"
git describe --tags                  # HEAD: 2 commits past v1.0.0
git describe --tags --long           # --long: always show the count, even on a tag
git describe --tags HEAD~2           # exactly on the tagged commit
Expected result — click to reveal
plain text
v1.0.0-2-gd255463
v1.0.0-2-gd255463
v1.0.0

What to read out of it: HEAD describes as v1.0.0-2-gd255463 — decoded: nearest tag v1.0.0, 2 commits beyond it, at abbreviated commit d255463 (your hash differs). That single string is a complete build identity: which release it descends from, how far past, and the exact commit. HEAD~2 — the commit v1.0.0 actually points at — describes as bare v1.0.0, because it is the tag. --long forces the count form even there (v1.0.0-0-g… on a tagged commit), which build scripts prefer for a uniform format. This is why CI can label a nightly build v1.0.0-2-gd255463 and you instantly know it's two commits ahead of the last release.

🎯 Interview questions — Part B

🎯 Corpus note — semantic versioning & git describe

In the surveyed interview corpus (GeeksforGeeks, Interview Coder), tagging questions concentrate on the annotated-vs-lightweight distinction (Part A); semantic versioning appears mostly as a mentioned concept rather than a verbatim standalone Q&A, and git describe is essentially absent as a named question. Rather than fabricate one, this Part carries a corpus note. What to be able to say cold: semver's MAJOR.MINOR.PATCH encodes a compatibility contract (breaking / feature / fix), and git describe turns any commit into a human-readable build identity relative to the last tag (<tag>-<N>-g<hash>). If an interviewer asks "how do you version releases and identify arbitrary builds," those two facts are the answer — and knowing that describe prefers annotated tags ties it back to Part A.

Part C — Pushing tags and the release flow

C1. Tags don't travel with commits

Here is the surprise that catches nearly everyone once: git push does not push tags. Commits go; tags stay home. You created v1.0.0, pushed your branch, and the tag is nowhere on the server — because tags are a separate namespace Git deliberately does not sync implicitly (you might have local scratch tags you never meant to share). Three ways to send them: git push origin <tagname> for one specific tag (the disciplined choice — you push exactly the release you mean), git push origin --tags for all local tags at once (convenient, but ships any junk tags too), and deleting a remote tag needs its own command: git push origin --delete <tagname>, because local git tag -d only touches your copy.

🧪 Exercise 12.4 — publish a release tag, then retract one
bash
cd ~/git-course && git init --bare -q bakery.git && cd kitchen
git remote add origin ~/git-course/bakery.git
git push -q -u origin main
git push origin v1.0.0               # push ONE tag deliberately
git push origin --tags               # push the rest (v0.1.0)
git push origin --delete v0.1.0      # retract a remote tag
Expected result — click to reveal
plain text
To /home/aisha/git-course/bakery.git
 * [new tag]         v1.0.0 -> v1.0.0
To /home/aisha/git-course/bakery.git
 * [new tag]         v0.1.0 -> v0.1.0
To /home/aisha/git-course/bakery.git
 - [deleted]         v0.1.0

What to read out of it: * [new tag] v1.0.0 -> v1.0.0 — the tag traveled only because you explicitly pushed it; the earlier git push -u origin main (which you ran) sent commits but no tags. --tags then pushed the remaining v0.1.0. The retraction (- [deleted]) mirrors Module 8's branch deletion and needs the same --delete because a local git tag -d would never have touched the server. This local-only-by-default behavior is the single most common tag surprise — "I tagged the release but the deploy can't find v1.0.0" is almost always an unpushed tag.

Now imagine this at 500 hosts. Tags are the anchor of the entire release-and-deploy machinery. Pushing an annotated tag is what triggers release pipelines — CI systems watch for tag pushes matching v* and, on one, build the artifact, run the release suite, publish to registries, and deploy; the tag name becomes the artifact version and the deployment identity. This is why the discipline matters at scale: an accidental --tags push of a local scratch tag can kick off a real release pipeline, and a tag that was never pushed means the deploy references a version the server has never heard of. Tag hygiene is release hygiene — the tag is the release trigger, the version string, and the rollback target (deploy the previous tag) all at once.

🎯 Interview questions — Part C

🎯 Corpus note — pushing tags

Tag pushing appears in the corpus only inside broader tag answers (usually as the "don't forget tags don't auto-push" caveat within the git tag -a question), not as a standalone verbatim item — so, a corpus note rather than a fabricated block. The one fact interviewers reliably probe as a follow-up: git push does not send tags — you need git push origin <tag> (one) or --tags (all), and remote deletion is git push origin --delete <tag>. Volunteering that caveat when asked about tagging, before it's asked, is itself the signal of someone who has actually shipped a release — because everyone hits the "my tag isn't on the server" surprise exactly once.

Part D — Signed tags: provable releases

D1. Signing and verifying

Module 1's trap: commit and tag identity are self-declared — anyone can tag as anyone. For code the world downloads and runs, "who really cut this release?" needs a cryptographic answer. git tag -s <name> creates a signed annotated tag: Git signs the tag object with your private key (GPG traditionally, or SSH keys in modern Git — git config gpg.format ssh with a user.signingkey), embedding a signature block. Anyone with your public key can then run git tag -v <name> to verify the signature recomputes — proving the tag was made by your key and the commit it names is unaltered. This is how Linux distributions, language runtimes, and security-conscious projects let users trust a release tarball actually came from the maintainers. The exercise below uses SSH signing (no GPG setup needed); skip it if you have no signing key — the concept is the interview content. The cryptography behind signatures — key pairs, trust, verification — is the subject of the SSL Certificates track in this same DevOps Learning library.

🧪 Exercise 12.5 — sign a release and verify it (SSH-key path)
bash
cd ~/git-course/kitchen
# One-time setup (skip if you already sign): make an SSH key and tell Git to use it
ssh-keygen -t ed25519 -f ~/.ssh/git_sign -N "" -C "[email protected]"
git config gpg.format ssh
git config user.signingkey ~/.ssh/git_sign.pub
git tag -s v1.1.0 -m "First signed release"      # -s: sign it
git cat-file -p v1.1.0 | head -9                 # the signature is IN the tag object
# To verify, Git needs to know which keys to trust (allowed-signers file):
printf '[email protected] namespaces="git" %s\n' "$(cat ~/.ssh/git_sign.pub)" > ~/allowed_signers
git config gpg.ssh.allowedSignersFile ~/allowed_signers
git tag -v v1.1.0                                 # verify
Expected result — click to reveal
plain text
object d2554633cb3640ee63d44fb0109cada88a3fae4d
type commit
tag v1.1.0
tagger Aisha Rahman <[email protected]> 1788819442 +0000

First signed release
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg...
Good "git" signature for [email protected] with ED25519 key SHA256:PHATihCoJvN9...

What to read out of it: the tag object now carries a -----BEGIN SSH SIGNATURE----- block inside it — the signature is part of the content-addressed object, so it cannot be detached or altered without changing the tag's hash (Module 3's integrity, now doing security work). git tag -v printed Good "git" signature for [email protected] — verification succeeded: the signature matches a trusted key, named in your allowed-signers file. Two failure modes are worth seeing on purpose. With gpg.ssh.allowedSignersFile unset entirely, git tag -v errors out with error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification — Git won't even attempt trust without a trust list. With the file configured but not containing this signer's key (e.g. verifying someone else's tag), you get the revealing pair: Good "git" signature with ED25519 key SHA256:… followed by No principal matched. — the math checks out, but Git has no trusted identity for that key. That gap — valid signature vs trusted signer — is exactly the distinction interviewers probe.

Counter-intuitive: a signature being cryptographically valid is not the same as the signer being trusted — and the gap is where security actually lives. When your allowed-signers file is configured but does not list a tag's signing key (the everyday case of verifying someone else's tag), git tag -v reports Good "git" signature with ED25519 key SHA256:… and No principal matched.: Git confirmed the math (this signature was made by some key) but had no basis to trust that key belongs to a legitimate maintainer. Trust comes from the out-of-band step — publishing maintainer public keys, an allowed-signers file, a GPG web of trust, or a hosting platform's "Verified" badge backed by uploaded keys. A forged tag can carry a perfectly valid signature from the attacker's key; only checking the key against a trusted set catches it. "Is the signature good?" and "do I trust this signer?" are two questions, and conflating them is the classic supply-chain blind spot.

🎯 Interview questions — Part D

🎯 Corpus note — signed tags

Signed/GPG tags appear in the corpus only as a mentioned option inside the git tag -a answer ("can be signed with -s"), not as a standalone verbatim question — hence a corpus note. The interview-ready substance: git tag -s signs an annotated tag with your key; git tag -v verifies it; the point is provable authorship of releases against a trusted public key, and the sharp distinction between a valid signature and a trusted signer (a valid signature from an untrusted key proves nothing about origin). Tie it to the supply-chain frame — signed tags and commits are how downstream consumers know a release genuinely came from the maintainers — and to Module 1's "identity is declared, not verified": signing is the mechanism that finally verifies it.

Part E — Production practice

E1. Symptom → cause → diagnosis → fix

Click the symptom you're seeing.

⚠️ Deploy/release pipeline can't find the tag you "just made"

What is really happening: git push doesn't send tags; the tag is local-only.

Diagnose: git ls-remote --tags origin (what the server actually has)

The fix: git push origin <tag> (or --tags); make tag-push part of the release runbook.

⚠️ git describe fails: fatal: No names found, cannot describe anything.

What is really happening: No annotated tag is reachable from this commit (and --tags was not passed for lightweight).

Diagnose: git tag (any tags at all?) · git describe --tags (include lightweight)

The fix: Create the release tags; use annotated so describe finds them without --tags.

⚠️ Deleted a tag locally but it's still on the server / still triggers builds

What is really happening: git tag -d is local only; the remote tag persists.

Diagnose: git ls-remote --tags origin

The fix: git push origin --delete <tag>; coordinate — others may have fetched it.

⚠️ Version sort puts v1.10.0 before v1.9.0

What is really happening: Alphabetical sort, not version sort.

Diagnose: git tag (default alpha) vs git tag --sort=-v:refname

The fix: Always use --sort=v:refname (or -v:refname for newest-first) for version tags.

⚠️ Two developers' clones disagree about what v2.0.0 points to

What is really happening: A tag was moved/re-created (-f) after being pushed — tags are meant to be immutable.

Diagnose: git ls-remote --tags origin vs local git rev-parse v2.0.0

The fix: Never re-point a published tag; cut a new version instead. If unavoidable, force-push + all-hands re-fetch.

⚠️ git tag -v says No principal matched despite a Good signature

What is really happening: The signature is valid but the signer isn't in your trusted set.

Diagnose: Check gpg.ssh.allowedSignersFile / imported GPG keys

The fix: Add the maintainer's public key to your allowed-signers/keyring — validity ≠ trust.

⚠️ A "small fix" release broke downstream auto-updates

What is really happening: A breaking change shipped as PATCH/MINOR — the semver contract was violated.

Diagnose: Review the change against the last release's public behavior

The fix: Bump MAJOR for any breaking change regardless of diff size; publish an errata/deprecation note.

E2. Capstone — four tickets

Ticket 1 — "Set up our release-tagging convention." A team ships a service continuously and wants a repeatable tagging procedure that drives CI and produces clean version strings.

Worked answer: annotated, semver, signed, pushed — as a scripted checklist. Cut releases with git tag -a v<MAJOR>.<MINOR>.<PATCH> -m "Release notes summary" on the exact commit being shipped (annotated for metadata + describe support; semver so the number carries compatibility intent — bump MAJOR on breaking changes without exception). Sign them (-s) if the artifact leaves the org or compliance wants provenance. Push specifically: git push origin v1.4.0 (never a blanket --tags from a dev machine — it ships scratch tags), which is what the release pipeline watches for to build/publish/deploy. Stamp every non-release build with git describe --tags --long so nightly artifacts read v1.4.0-7-gabc1234 — unambiguous lineage. Enforce with a tag-protection rule on the host (only maintainers/CI may push v* tags) so the release trigger can't be pulled by accident. One page, and it wires tagging into the whole delivery chain.

Ticket 2 — "We forgot to tag v2.3.0 and it's already in production." The release went out three commits ago; the deploy log shows the exact commit hash. Now what?

Worked answer: tags name any commit, so retro-tagging is trivial and correct: git tag -a v2.3.0 <deployed-hash> -m "Release 2.3.0 (tagged retroactively)", then git push origin v2.3.0. Verify it lands where the deploy actually was (git describe --tags <deployed-hash> should now print exactly v2.3.0). Two honest caveats: annotated tags record the tagger's date (now), not the release date — so note the real ship date in the message for the record; and if a CI pipeline triggers on tag push, a retroactive tag may kick off a build/publish for an already-deployed version — check whether that's harmless (idempotent republish) or needs the pipeline skipped for this tag. Prevent recurrence: make "cut and push the tag" a gated step in the deploy pipeline, not a human afterthought — the whole class of "forgot to tag" bugs comes from tagging being manual.

Ticket 3 — "Make our open-source releases verifiable." Downstream users and a security audit want to confirm release tarballs genuinely come from the maintainers, not a compromised mirror.

Worked answer: signed tags plus published trust anchors. Maintainers create releases with git tag -s v<x> -m "…" using keys the project has published (in the repo's SECURITY.md, a keyserver, and uploaded to the host so commits/tags show "Verified"). Consumers verify with git tag -v v<x> against those keys — and the audit-critical teaching point (the counter-intuitive callout, as a deliverable): a Good signature alone is worthless without checking the key is a trusted maintainer key — a compromised mirror can present a validly-signed tag from an attacker's key. So document the exact trusted key fingerprints out-of-band, and instruct verifiers to confirm the signer matches, not merely that the signature is valid. Layer in reproducible builds if the tarball (not just the tag) must be verifiable, and tag-protection so only maintainer keys can push v*. The one-sentence summary for the audit: signing proves who, trust anchoring proves which who — you need both.

Ticket 4 — "Someone force-moved v1.5.0 to a hotfix and now clones disagree." A maintainer did git tag -f v1.5.0 <newhash> && git push --force origin v1.5.0 to "include a last-minute fix." Half the team's clones still point v1.5.0 at the old commit; a customer built from the old one.

Worked answer: the root error is treating a published tag as mutable — tags are immutable release identities; moving one means "v1.5.0" now refers to two different snapshots depending on who you ask, which is exactly the integrity that versioning exists to prevent. Damage control: decide the single canonical v1.5.0, force-push it once more so the server is authoritative, and have everyone git fetch --tags --force to converge (fetch does not update an existing local tag without --force — another reason this hurts). Then communicate the collision to anyone who may have built the old one. The real fix is procedural: the last-minute fix should have been v1.5.1, not a moved v1.5.0 — cutting a new version is free and honest; re-pointing a shipped tag is a supply-chain hazard. Add host tag-protection making published tags non-force-pushable so this is impossible, not merely discouraged. The interview close: immutability of released tags is a contract with everyone downstream, and "just move the tag" silently breaks it.

E3. Documentation reference

TopicOfficial sourceWhat it covers
Tagging (tutorial)Git Book §2.6 — TaggingLightweight vs annotated, pushing, checking out tags
git taggit-tag manual-a/-m/-s/-v/-d/-l/--sort, tagging past commits
git describegit-describe manual--tags, --long, --match, the <tag>-<N>-g<hash> format
Semantic Versioningsemver.orgThe MAJOR.MINOR.PATCH contract in full, with pre-release rules
Signing your workGit Book §7.4 — Signing Your WorkSigned tags and commits, GPG/SSH setup, verification

E4. Self-assessment

Try each question aloud, from memory, before opening its answer — the gap between your answer and the hidden one is what to study.

1. How does a tag differ from a branch in what it does over time?

A branch is a moving pointer that advances as you commit; a tag never moves. It pins a name to one specific commit forever — "this exact snapshot is v1.0.0."

2. Lightweight vs annotated: what object does each resolve to, and which can be signed?

A lightweight tag is just a ref — git cat-file -t resolves it straight to a commit; it has no object of its own and no metadata. An annotated tag is a real tag object in the database, storing the tagger's name, email, date, and message, which then points at the commit. Only annotated tags can be signed — one more reason releases should be annotated.

3. Which Git object type is Module 3's "fourth type," and which tag kind creates one?

The tag object — alongside blob, tree, and commit, all content-addressed and all readable with cat-file. Only an annotated tag (git tag -a) creates one; a lightweight tag is a bare ref under .git/refs/tags/ with no object of its own.

4. How do you tag a commit from last week that was never tagged?

git tag -a <name> <commit> — tags name any commit, past or present, so retro-tagging is trivial and correct. Find the deployed hash (deploy log, git log), tag it, then push the tag with git push origin <tag>. One caveat: annotated tags record the tagger's date (now), not the release date — note the real ship date in the message.

5. Why does git tag sort wrong for versions, and what flag fixes it?

Plain git tag sorts alphabetically, which puts v1.10.0 before v1.9.0. Use --sort=v:refname (or -v:refname for newest-first) to sort by real version number.

6. State the semver bump rule for a bug fix, a new backward-compatible feature, and a breaking change.

Backward-compatible bug fix: bump PATCH (1.4.2 → 1.4.3). Backward-compatible new feature: bump MINOR (1.4.2 → 1.5.0). Breaking change that requires consumers to adapt: bump MAJOR (1.4.2 → 2.0.0).

7. Why is "it's a small change" not a valid reason to avoid a MAJOR bump?

Small ≠ compatible — a one-character change to a default value can break every consumer, and if it went out as a patch, their auto-updates just broke in production. The version number is an API contract with everyone who depends on you: bump MAJOR whenever behavior consumers rely on changes, regardless of diff size. Teams that treat semver casually teach their users to pin exact versions and never auto-update, which defeats the entire point.

8. Decode v1.0.0-2-gd255463 field by field. What produces it?

Nearest reachable tag v1.0.0, 2 commits beyond it, at abbreviated commit d255463 — the g prefix is for "git." git describe produces it: a complete build identity saying which release the commit descends from, how far past it, and the exact commit. This is how CI stamps version strings on artifacts automatically.

9. What surprising thing does git push not do, and what are the two ways to fix it?

git push does not push tags — commits go, tags stay home, because tags are a separate namespace Git deliberately does not sync implicitly. Fix with git push origin <tagname> for one specific tag (the disciplined choice) or git push origin --tags for all local tags at once (convenient, but ships any junk scratch tags too).

10. How do you delete a tag that's already on the remote — and why isn't git tag -d enough?

git push origin --delete <tagname>. git tag -d only deletes your local copy; the remote tag persists (and can still trigger builds) until you delete it on the server with its own command — and coordinate, because others may have fetched it.

11. git tag -s then git tag -v: what does each do, and what does a "Good signature" not guarantee?

git tag -s creates a signed annotated tag: Git signs the tag object with your private key, embedding the signature block inside the content-addressed object. git tag -v verifies that the signature recomputes against a public key. What a "Good signature" does not guarantee is trust: it proves the math — some key made the signature — not that the key belongs to a legitimate maintainer; without a matching entry in your allowed-signers file you get No principal matched. Validity ≠ trust — conflating them is the classic supply-chain blind spot.

12. Why is re-pointing a published tag a supply-chain hazard, and what should you do instead?

Published tags are immutable release identities; moving one means "v1.5.0" refers to two different snapshots depending on who you ask — and git fetch won't even update an existing local tag without --force, so clones silently disagree while downstream users may have built the old commit. Cut a new version instead (the last-minute fix should be v1.5.1, not a moved v1.5.0) — cutting a new version is free and honest. Add host tag-protection so published tags are non-force-pushable, making the mistake impossible rather than merely discouraged.

E5. Sources

Interview questions in this module were captured verbatim from: GeeksforGeeks — Top 70+ Git Interview Questions (updated Jul 30, 2026) and Interview Coder — 90+ Common Git Interview Questions (Sep 20, 2025). A corpus note: the published corpus covers annotated-vs-lightweight and git tag -a as verbatim questions but treats semver, git describe, tag-pushing, and signing only inside broader answers — so Parts B, C, and D carry honest corpus notes rather than fabricated interview blocks (the track's rule: never invent a question and present it as published). Technical claims were verified against the official documentation in E3; every command and output on this page was executed on Git 2.43.0 on Linux, including the signed-tag flow (SSH signing with an ed25519 key). Commit hashes, tag object IDs, key fingerprints, and dates will differ on your machine; signing setup (GPG vs SSH) varies, so check git config --get gpg.format on yours.

🗒️ Cheat sheet — Module 12

CommandWhat it does
git tag <name>Lightweight tag — a bare ref to a commit, no metadata
git tag -a <name> -m "msg" · ... <commit>Annotated tag (object w/ tagger, date, message) · on a specific past commit
git tag · git tag -l "v1.*" -n1 · --sort=-v:refnameList · filter with messages · sort by version (not alpha!)
git tag -d <name> · git push origin --delete <name>Delete locally · delete on the remote (separate command)
git show <tag> · git cat-file -p <tag>Tag + its commit · the raw annotated-tag object
git describe --tags · --long<tag>-<N>-g<hash> build identity · always include the count
git push origin <tag> · git push origin --tagsPush one tag (deliberate) · push all local tags (ships scratch too)
git tag -s <name> -m "msg" · git tag -v <name>Signed annotated tag · verify its signature (needs trusted key)
git ls-remote --tags originWhat tags the server actually has (diagnose unpushed/undeleted tags)
git fetch --tags · ... --forceFetch tags · overwrite a moved tag (fetch won't move existing tags otherwise)

Key concepts: a tag is a permanent, non-moving name for one commit · lightweight = bare ref; annotated = a real object (Git's 4th type) with tagger/date/message, signable, preferred by describe — use annotated for releases · tag any commit, past or present · sort versions with --sort=v:refname, never plain alpha · semver MAJOR.MINOR.PATCH = a compatibility contract (breaking/feature/fix); bump MAJOR for any breaking change regardless of size · git describe = <tag>-<commits-since>-g<hash>, the auto build-version · git push does NOT push tags — push origin <tag> or --tags; remote delete needs --delete · signed tags (-s/-v) prove authorship, but a valid signature ≠ a trusted signer · published tags are immutable — cut a new version, never move a shipped tag.

Next: Module 13 — Searching and Debugging History — you can now name and mark any commit. Module 13 hunts through them: git blame for line-by-line authorship, git bisect (the binary-search bug finder), the pickaxe (log -S/-G) for content archaeology, and git grep for searching the tree itself.
Spotted a mistake or want something added? Send me a note.