In Git, why don't tags travel with a normal git push, and what does --follow-tags do?
answer
- Push moves only the refs it names
- Tags live in a different ref namespace
- One flag is blunt, another is selective
- Reachability from the pushed commits matters
- Only one kind of tag qualifies for the selective flag
basics
~20 sA push updates only the refs named by its refspec, which for a branch push means refs/heads/… — refs/tags/… is never implied. git push --follow-tags additionally sends annotated tags that are reachable from the pushed commits and missing on the remote.
solid answer
~40 sPush is a ref-update operation: it moves exactly the refs its refspec names. Pushing `main` updates `refs/heads/main` on the remote and nothing else, so a tag you created stays local even though the commit it points at was transferred. Three ways to publish tags: `git push origin v1.0` (or the explicit `refs/tags/v1.0`) for one; `git push --tags`, which sends every ref under `refs/tags/`; and `git push --follow-tags`, which sends only **annotated** tags reachable from the history being pushed and not already on the remote. That last one is the sensible default for release work, and `push.followTags = true` makes it automatic. Note the annotated-only restriction: lightweight tags are ignored by `--follow-tags`, so a lightweight release tag simply never leaves your machine.
code
console · 7 lines$ git tag -a v2.0 -m "Release 2.0"
$ git push origin main
To example:repo.git
1a2b3c4..9f8e7d6 main -> main
$ git push --follow-tags origin main
* [new tag] v2.0 -> v2.0go deeper
Recall that tags need their own push — by name, with --tags, or with --follow-tags — because a branch push does not carry them.
Explain it through refspecs: push updates only the refs it names, and refs/tags is a separate namespace. Distinguish --tags from --follow-tags precisely.
Volunteer the annotated-only restriction as the silent failure mode in release automation, and set push.followTags so tag publication stops depending on anyone remembering a flag.
Treat the tag namespace as a published, effectively immutable contract: standardise annotated or signed release tags, automate their publication, and treat moving a tag as a breaking change rather than a fixup.
## Why tags are not implied Every push is a set of ref updates derived from refspecs. `git push origin main` expands to something like `refs/heads/main:refs/heads/main` — one ref. The commit objects a tag points at may well be transferred as part of that branch's history, but the *tag ref* is a separate entry in `refs/tags/` and nothing in the branch refspec mentions it. That is why the objects arrive and the tag does not: the remote has the commit, but no name pointing at it. This is deliberate. Tags are a global, largely immutable namespace shared by everyone who clones. Publishing them by accident, or moving one that others already fetched, causes far more trouble than a stray branch, so Git makes tag publication explicit. ## Three ways to publish - **One tag by name:** `git push origin v1.0`. Git resolves the short name; `git push origin refs/tags/v1.0:refs/tags/v1.0` is the unambiguous long form and is worth using when a branch of the same name could exist. - **All tags:** `git push --tags` sends every ref under `refs/tags/`, in addition to any refspecs given on the command line. Blunt but occasionally what you want on a fresh remote. On a long-lived repository it can publish years of local experiment tags. - **Reachable annotated tags:** `git push --follow-tags` sends annotated tags that are (a) reachable from the commits being pushed and (b) not already present on the remote, alongside the branch update. This is the surgical option: pushing `main` also publishes the release tags that belong to the history you just published, and nothing else. ## Annotated versus lightweight matters here An annotated tag is a real tag object carrying a tagger, a date, a message, and optionally a signature; a lightweight tag is just a ref pointing straight at a commit. `--follow-tags` deliberately considers only annotated tags, on the reasoning that lightweight tags are private bookmarks while annotated ones are published markers. The practical consequence bites people: create a release tag with plain `git tag v1.0`, push with `--follow-tags`, and the tag does not appear on the remote at all, with no error. Creating release tags with `git tag -a` (or `-s` for a signed one) avoids this entirely. ## Making it the default `push.followTags = true` applies `--follow-tags` to every push. This is a common team setting: release tags reach the remote with the commits they describe, without anyone remembering a flag, and without the shotgun effect of `--tags`. ## Updating and deleting tags on the remote Moving a published tag is hostile: clones that already fetched it do not silently adopt a new value, so different people end up with different meanings for the same name. The receiving side reflects this by refusing to update an existing tag ref unless the update is forced. Creating a *new* tag name is an ordinary update and needs no force. Deleting a remote tag uses the same forms as a branch: `git push origin --delete v1.0` or the refspec `git push origin :refs/tags/v1.0`. Deleting the remote copy does not remove anyone's local copy. ## Diagnosing "the tag isn't there" The checklist is short. Is the tag annotated (`git cat-file -t v1.0` prints `tag` for annotated, `commit` for lightweight)? Is it reachable from the branch you pushed? Did the push actually include it (`git push --dry-run --follow-tags origin main` lists the refs that would move)? And does the remote already have a *different* tag of that name, in which case the update is refused rather than silently skipped? ## The interview point The question tests whether you think of push as "upload my repository" or as "update these named refs". The strong answer names the refspec reason, distinguishes `--tags` from `--follow-tags`, volunteers the annotated-only restriction as the practical gotcha, and mentions `push.followTags` as the way to stop relying on memory.
- You pushed with --follow-tags but your new tag is still missing. What is the likely cause?It is probably a lightweight tag. `--follow-tags` sends only annotated tags — real tag objects created with `git tag -a` or `-s` — and silently ignores lightweight refs. Check with `git cat-file -t v1.0`: `tag` means annotated, `commit` means lightweight. The other possibility is that the tag is not reachable from the commits you pushed.
- How does --tags differ from --follow-tags?`--tags` pushes every ref under `refs/tags/` in addition to any refspecs on the command line, including old local experiment tags. `--follow-tags` pushes only annotated tags that are reachable from the history being pushed and missing on the remote. The second is what you want alongside a release branch push; the first is a blunt instrument.
- Why does updating an existing tag on the remote require a force?Because tags are treated as immutable published names: clones that already fetched the tag keep their value, so a moved tag means different people resolve the same name to different commits. The receiving side therefore refuses to overwrite an existing tag ref unless the update is explicitly forced. Creating a new tag name needs no force.
saying these in an interview costs you the question
- Assumes push uploads all local refs including tags
- Thinks --tags and --follow-tags are equivalent
- Does not know --follow-tags ignores lightweight tags
- Believes tags are pushed because their commits were
- Thinks moving a published tag updates everyone's clone