Git's two identity systems

May 18, 2026

I published an essay about building on git’s primitives and posted it on r/programming. It got decent traction, around 60 upvotes and some good technical discussion. Then the post was flagged as LLM-generated and removed by the moderators, even though the essay explicitly discloses that I use AI assistance for rendering under a bounded authoring protocol. I originate the ideas, AI helps with prose. That distinction didn’t matter to the moderation policy.

The removal stung, but some of the conversations that happened before it went down were worth preserving. One in particular stuck with me. Someone questioned whether I had actually written the article, then challenged a specific design decision: why does git-native-issue use UUIDs for issue identity instead of the SHA of the root commit?

The accusation was wrong. But the technical question was genuinely interesting, and answering it forced me to understand something about git that I hadn’t articulated before.

The boring choice

When I designed git-native-issue, I used UUIDs for issue identity without thinking twice. Each issue lives under refs/issues/<uuid>. The UUID is generated at creation time, independently, on any clone, with zero coordination.

I didn’t consider using SHAs. UUIDs are the standard distributed identity mechanism. Generate first, create the resource second. No coordination needed, negligible collision risk, and no central counter to manage. It was the normal distributed-systems answer.

The challenge

The question was: why not use the SHA of the root commit as the issue ID? Since every issue starts with a root commit, that SHA exists and is unique. Why introduce an external identity scheme when git already has content-addressed hashing?

The challenger pressed harder. He argued that with SHA-based identity, disconnected reporters filing the same bug would produce the same ID. Content-addressable deduplication, for free. He framed it as an obvious design flaw: “Do you understand how git commits are calculated from the SHA of the object? Why would you want to decouple the ID from the content, when git’s core design does not do so?”

But the premise was doing more work than it seemed.

My first answer was that you’d have a chicken-and-egg problem: you need the ID to name the ref, but you need the commit to get the SHA. That’s the distributed systems instinct talking. But it’s actually wrong here. git commit-tree returns the SHA before you create the ref. I could use it.

So why didn’t I?

How git actually handles identity

I find it useful to think of git as having two identity systems. Git’s own documentation doesn’t name it this way, but the architecture has it.

Objects are content-addressed. A blob’s SHA is the hash of its content. A tree’s SHA is the hash of its entries. A commit’s SHA is the hash of everything in it: tree, parents, author, committer, timestamps, message, and signature if present. Change any byte, get a different SHA. This is the layer most people think about when they think about git identity.

Refs are name-addressed. A branch called main is a file containing a SHA. The name “main” is not derived from any content. You choose it. You could call it anything. Rename a branch and zero objects change. I tested this: the object store before and after git branch -m main renamed is byte-identical. Only the ref file changes.

These are two different identity systems living in the same tool. Objects are addressed by what they contain. Refs are addressed by what you call them.

refs/heads/main is a name-addressed pointer to a content-addressed object. A tag ref works the same way: in the lightweight case it points directly to a commit, in the annotated case it points to a tag object. And refs/issues/<uuid> follows the exact same pattern.

Why git needs both

At first glance, a ref looks like just an alias for a SHA. “main” is a friendly name for abc123. But an alias is a fixed mapping. A ref is a mutable pointer. “main” today means abc123. After your next commit, it means def456. The ref moved. The objects didn’t.

That’s the reason both systems exist. Objects are immutable, so content-addressing works perfectly for them. But development is mutable. “Where is main right now?” changes with every commit. You need something that can move while the things it points to stay fixed.

I tested this directly: rename a branch with git branch -m main renamed and the object store is byte-identical before and after. Not a single object changes. The only thing that changes is the ref. The two layers are genuinely independent.

Content-addressing gives you integrity: you can verify that an object hasn’t been tampered with by recomputing its hash. Name-addressing gives you continuity: you can follow an evolving history through a stable name. Git needs both because software development involves both immutable artifacts (commits, snapshots) and mutable processes (branches moving forward, tags marking releases, issues accumulating events).

Why this matters for issues

An issue is not a single object. It’s a living thing. It grows with comments, state changes, label edits. Each of those events is a new commit appended to the chain. The ref moves forward, but the ref name stays the same.

A commit is immutable. An issue is not. Content-addressed identity makes sense for immutable things. For something that evolves, you need a stable name that doesn’t depend on the content at any particular moment. Refs solve this for branches, tags solve it for releases, and refs/issues/<uuid> solves it for issues. Same mechanism, new namespace.

The UUID is not replacing git’s content identity. It sits one layer above it. The issue ID names the evolving process; each event in that process remains content-addressed by git. The issue gets continuity from the ref and integrity from the commits. That is how the two identity systems compose.

Using the root commit’s SHA as the ref name would technically work. The root doesn’t change. But it couples the issue’s identity to one moment in its history, and it gains you nothing in return.

The dedup argument doesn’t survive the real world

The strongest case for SHA-based identity was deduplication: two reporters filing the same bug would produce the same ID.

I tested it. git commit-tree embeds the author name, email, and timestamp into the commit object. All of those go into the SHA computation. Two people filing the same bug one second apart get different SHAs. Two people filing it at the exact same second but with different git configs get different SHAs.

# Same content, 1 second apart
$ sha1=$(git commit-tree -- "$empty_tree" < bug.txt)
$ sleep 1
$ sha2=$(git commit-tree -- "$empty_tree" < bug.txt)
$ [ "$sha1" = "$sha2" ] && echo SAME || echo DIFFERENT
DIFFERENT

For SHA-based dedup to work, you’d need identical content, identical author identity, and identical timestamps to the second. Even then, there’s a more basic problem: two humans reporting the same bug will write it differently. Different wording, different emphasis, different level of detail. Content-addressable deduplication of bug reports is a theoretical property that doesn’t survive contact with how humans actually report bugs. Deduplication is a human judgment call regardless of the ID scheme. That’s true on GitHub, Jira, or here.

The edge case

There is one ref namespace in git where a SHA appears in the ref name: refs/replace/<sha>. These are object replacement mappings, where the ref name contains the SHA of the object being replaced. But this is a special-purpose index by object ID, not a general identity pattern. Most ref namespaces follow the ordinary name-addressed pattern.

Sometimes the right answer is boring

I could have used SHAs. It would have worked. It would have added hashing ceremony without buying anything that matters. UUID was the boring choice, and in this case, boring was correct.

This is the part where I’m supposed to zoom out and say something philosophical about engineering intuition, ceremony versus rigor, or the nature of good design. I’ll resist. It was just a UUID.

git-native-issue is open source: github.com/remenoscodes/git-native-issue


Authoring note. Drafting assistance by Claude. All arguments, examples, judgments, and claims are the author’s. AI was used for rendering (organizing, rephrasing, tightening), not for originating the thesis or authoring claims.