{"article":{"slug":"on-git-refs","title":"On Git Refs","subtitle":null,"summary":"matklad explains the mental model that made Git click for him: besides an immutable content-addressed object store, Git is a plain mutable key-value store of refs. Branches, tags, remote-tracking branches and notes are all just refs, and spelling out CLI shorthands like git fetch origin master shows why one command says origin master and another origin/master.","content_type":"blog_post","language":"en","canonical_url":"https://matklad.github.io/2026/10/07/git-ref.html","author":{"name":"matklad","url":"https://matklad.github.io/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"matklad.github.io","url":"https://matklad.github.io/","listing_slug":null,"listing":null},"topics":[{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Developer Tools","slug":"developer-tools","url":"https://listedarticles.com/topics/developer-tools"},{"name":"Software Engineering","slug":"software-engineering","url":"https://listedarticles.com/topics/software-engineering"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1098,"reading_minutes":5,"published_at":"2026-10-07T00:00:00.000Z","added_at":"2026-10-07T20:15:32.573Z","updated_at":"2026-10-07T20:15:32.573Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/on-git-refs","markdown_url":"https://listedarticles.com/articles/on-git-refs.md","example":false,"citation":"matklad, matklad.github.io. \"On Git Refs.\" 7 Oct 2026. https://matklad.github.io/2026/10/07/git-ref.html (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://matklad.github.io/2026/10/07/git-ref.html"},"body_markdown":"Oct 7, 2026\n\nI have recently improved my mental model of Git. Consider these two git commands:\n\n```\n$ git fetch origin master\n$ git switch -c my-feature origin/master\n```\n\nDo you understand why is it `origin master` in one command, and `origin/master`\nin the other? I didn’t, until a few weeks ago!\n\nMy understanding was that git is a content-addressable database. Git stores\ncommits, a commit is identified by the hash of its content, and the content\nof a commit is, primarily:\n\n* a memory-less snapshot of a state of the codebase at a given point in time,\n* a list of (hashes of) parent commits.\n\nThat was enough git for me to understand `git log` output and get me out of any\nbotched rebase without having to re-clone the repo (For roughly half of my\ncareer, I *was* re-cloning the repo. No shame in that! Learning git is useful,\nbut it’s not the *highest* priority thing to learn when you start).\n\n---\n\nI now understand that git not only comes with an append-only (“immutable”)\ncontent-addressable database, but is also a boring mutable key-value store.\n\nGit has a mutable map whose keys are strings, and whose values are\ncontent-addressed objects. The keys are conventionally formatted as file system\npaths, and you can usually inspect the state of the mapping by listing\n`.git/refs` directory:\n\n```\n$ eza -T .git/refs\n.git/refs\n├── heads\n│   ├── make\n│   ├── master\n│   ├── my-feature\n│   └── pbd-adt\n├── origin\n├── remotes\n│   └── origin\n│       ├── context-switches\n│       ├── gh-pages\n│       ├── HEAD\n│       ├── make\n│       └── master\n└── tags\n\n$ cat .git/refs/heads/master\nb59148228e52f7c615ead7fdd4e91001994ad50f\n\n$ git show-ref refs/heads/master\nb59148228e52f7c615ead7fdd4e91001994ad50f refs/heads/master\n```\n\nWhat makes this `refs` KV infrastructure confusing is that:\n\n* It powers many distinct user-visible git features, but refs themselves are an\n  implementation detail.\n* It is a bit of a leaky abstraction, refs are *almost* invisible in the\n  day-to-day usage.\n* Git CLI uses shorthand notation for refs and many default arguments, which\n  makes it not obvious that a particular CLI argument is a ref.\n* And, as usual, git likes to give several names to one thing, and re-uses the\n  same name for distinct things.\n\nBranches, tags, and git notes are all just refs!\n\n---\n\nThe structure becomes much more obvious once you elaborate all CLI shortcuts. The original command\n\n```\n$ git fetch origin master\n```\n\nthen becomes\n\n```\n$ git fetch \\\n    https://github.com/matklad/matklad.github.io \\\n    refs/heads/master:refs/remotes/origin/master\n```\n\nThe first argument of `fetch` (`https://...`) is a location of a remote\nrepository. Git will “dial” that address, and will transfer some data from that\ncomputer locally over the network.\n\nThe second argument is a `source:target` pair of string keys (refs). The\n`source` is a key on the remote repo, the `target` is the name of a local key,\nand fetch as a whole asks git to read a value from a remote repository and save\nit locally under a different name.\n\nTo avoid typing repository URLs all the time, git assigns them symbolic names,\nwith `origin` being the conventional name for the primary remote repository:\n\n```\n$ git fetch origin \\\n    refs/heads/master:refs/remotes/origin/master\n```\n\n`refs/heads/master` is a fully elaborated name of a branch on the remote repo.\nThat is, branch `my-feature` is just a\n`refs/heads/my-feature`\nref. It could\nhave been\n`refs/branch/my-feature`,\nbut it isn’t :)\n\nI don’t know the specific shorthand rules, but, generally, git allows you to\nspell only the suffix of a ref:\n\n```\n$ git fetch origin \\\n    master:refs/remotes/origin/master\n```\n\n`refs/remotes/origin/master` is the name of the local ref we’ll use to store the\nresult. It would seem natural to just use the same name locally as the one on\nthe remote, but this only works if there’s a single remote. If there are two\nupstream repositories (for example, your fork, and the original repo you forked\nfrom), their ref names will collide. That’s why we want to namespace the refs\nfor remote called `foo` under `refs/remotes/foo`. And `origin` is just a\nconventional name for *the* remote in simple setups.\n\nAgain, it would be more natural to *directly* mirror remote ref structure\nlocally:\n\n```\nrefs/ heads/my-branch -> refs/ remotes/origin/ heads/my-branch\n```\n\nbut git strips the redundant heads component. And this `-heads`,\n`+remotes/$remote` mapping is built in, which compresses the command to\n\n```\n$ git fetch origin master\n```\n\nIt’s worth reflecting *why* it works this way. Git model is offline first.\nWhat’s more, it assumes *explicit* synchronization points. Rather than\nsynchronizing with the remote repository in background when there’s\nconnectivity, git requires explicit `fetch` and `push` operations to transfer\nbytes over the wire. In this paradigm, it is useful to model the state of the\nremote party at the moment when we talked to them the last time. Theory of mind!\n\nThis *hopefully* deconfuses git’s concept of local and remote branches. Consider\nthe `main` branch. It exists on the remote named `origin` as `refs/heads/main`.\nWhen you synchronize your local repository with `origin`, you get\n`refs/remotes/origin/main` —\nyou current best knowledge about the the state of\n`main` on the `origin`.\n\nAnd then there’s your local `refs/heads/main`. It typically starts pointing at\nthe same commit as\n`refs/remotes/origin/main`.\nBut, when you make a commit, `refs/heads/main` advances, but\n`refs/remotes/origin/main`\nstays the same.\n\nWhen you try to push your local commit to origin, you will get a conflict, if\nthe `main` branch on the `origin` advanced in the meanwhile. In that case, git\nautomatically updates\n`refs/remotes/origin/main`\n(as that’s just a local mirror of the remote state), but then it’s on you to\nupdate `refs/heads/main` and push it again.\n\nRevisiting the full example:\n\n```\n$ git fetch origin master\n$ git switch -c my-feature origin/master\n```\n\nThe first command looks up the URL for the `origin` remote in `.git/config` and\nmakes a network request to that machine. As a result, the local\n`refs/remotes/origin/master` gets updated to the same commit as\n`refs/heads/master` remotely (the commit and its ancestors are transferred\nlocally as a result).\n\nThe second command creates a `refs/heads/my-feature` ref (a branch), whose\nstarting point is `refs/remotes/origin/master`. It is an example of a leaky\nabstraction.\n\nThe second argument there is a (shorthand of a) ref, so you can do\n\n```\n$ git switch -c my-feature \\\n    refs/remotes/origin/master\n```\n\nBut, although the first argument *creates* a ref, it isn’t a ref itself. In\nother words, if you try to elaborate it as well\n\n```\n$ git switch -c refs/heads/my-feature \\\n    refs/remotes/origin/master\n```\n\nyou’ll get\n\n```\nrefs/heads/refs/heads/my-feature\n```\n\nThat’s all! I am pretty sure this isn’t particularly useful, but maybe it is\ninteresting!\n","body_html":"<p>Oct 7, 2026</p>\n<p>I have recently improved my mental model of Git. Consider these two git commands:</p>\n<pre><code>$ git fetch origin master\n$ git switch -c my-feature origin/master</code></pre>\n<p>Do you understand why is it <code>origin master</code> in one command, and <code>origin/master</code>\nin the other? I didn’t, until a few weeks ago!</p>\n<p>My understanding was that git is a content-addressable database. Git stores\ncommits, a commit is identified by the hash of its content, and the content\nof a commit is, primarily:</p>\n<ul><li>a memory-less snapshot of a state of the codebase at a given point in time,</li><li>a list of (hashes of) parent commits.</li></ul>\n<p>That was enough git for me to understand <code>git log</code> output and get me out of any\nbotched rebase without having to re-clone the repo (For roughly half of my\ncareer, I <em>was</em> re-cloning the repo. No shame in that! Learning git is useful,\nbut it’s not the <em>highest</em> priority thing to learn when you start).</p>\n<hr />\n<p>I now understand that git not only comes with an append-only (“immutable”)\ncontent-addressable database, but is also a boring mutable key-value store.</p>\n<p>Git has a mutable map whose keys are strings, and whose values are\ncontent-addressed objects. The keys are conventionally formatted as file system\npaths, and you can usually inspect the state of the mapping by listing\n<code>.git/refs</code> directory:</p>\n<pre><code>$ eza -T .git/refs\n.git/refs\n├── heads\n│   ├── make\n│   ├── master\n│   ├── my-feature\n│   └── pbd-adt\n├── origin\n├── remotes\n│   └── origin\n│       ├── context-switches\n│       ├── gh-pages\n│       ├── HEAD\n│       ├── make\n│       └── master\n└── tags\n\n$ cat .git/refs/heads/master\nb59148228e52f7c615ead7fdd4e91001994ad50f\n\n$ git show-ref refs/heads/master\nb59148228e52f7c615ead7fdd4e91001994ad50f refs/heads/master</code></pre>\n<p>What makes this <code>refs</code> KV infrastructure confusing is that:</p>\n<ul><li><p>It powers many distinct user-visible git features, but refs themselves are an</p><p>implementation detail.</p></li><li><p>It is a bit of a leaky abstraction, refs are <em>almost</em> invisible in the</p><p>day-to-day usage.</p></li><li><p>Git CLI uses shorthand notation for refs and many default arguments, which</p><p>makes it not obvious that a particular CLI argument is a ref.</p></li><li><p>And, as usual, git likes to give several names to one thing, and re-uses the</p><p>same name for distinct things.</p></li></ul>\n<p>Branches, tags, and git notes are all just refs!</p>\n<hr />\n<p>The structure becomes much more obvious once you elaborate all CLI shortcuts. The original command</p>\n<pre><code>$ git fetch origin master</code></pre>\n<p>then becomes</p>\n<pre><code>$ git fetch \\\n    https://github.com/matklad/matklad.github.io \\\n    refs/heads/master:refs/remotes/origin/master</code></pre>\n<p>The first argument of <code>fetch</code> (<code>https://...</code>) is a location of a remote\nrepository. Git will “dial” that address, and will transfer some data from that\ncomputer locally over the network.</p>\n<p>The second argument is a <code>source:target</code> pair of string keys (refs). The\n<code>source</code> is a key on the remote repo, the <code>target</code> is the name of a local key,\nand fetch as a whole asks git to read a value from a remote repository and save\nit locally under a different name.</p>\n<p>To avoid typing repository URLs all the time, git assigns them symbolic names,\nwith <code>origin</code> being the conventional name for the primary remote repository:</p>\n<pre><code>$ git fetch origin \\\n    refs/heads/master:refs/remotes/origin/master</code></pre>\n<p><code>refs/heads/master</code> is a fully elaborated name of a branch on the remote repo.\nThat is, branch <code>my-feature</code> is just a\n<code>refs/heads/my-feature</code>\nref. It could\nhave been\n<code>refs/branch/my-feature</code>,\nbut it isn’t :)</p>\n<p>I don’t know the specific shorthand rules, but, generally, git allows you to\nspell only the suffix of a ref:</p>\n<pre><code>$ git fetch origin \\\n    master:refs/remotes/origin/master</code></pre>\n<p><code>refs/remotes/origin/master</code> is the name of the local ref we’ll use to store the\nresult. It would seem natural to just use the same name locally as the one on\nthe remote, but this only works if there’s a single remote. If there are two\nupstream repositories (for example, your fork, and the original repo you forked\nfrom), their ref names will collide. That’s why we want to namespace the refs\nfor remote called <code>foo</code> under <code>refs/remotes/foo</code>. And <code>origin</code> is just a\nconventional name for <em>the</em> remote in simple setups.</p>\n<p>Again, it would be more natural to <em>directly</em> mirror remote ref structure\nlocally:</p>\n<pre><code>refs/ heads/my-branch -&gt; refs/ remotes/origin/ heads/my-branch</code></pre>\n<p>but git strips the redundant heads component. And this <code>-heads</code>,\n<code>+remotes/$remote</code> mapping is built in, which compresses the command to</p>\n<pre><code>$ git fetch origin master</code></pre>\n<p>It’s worth reflecting <em>why</em> it works this way. Git model is offline first.\nWhat’s more, it assumes <em>explicit</em> synchronization points. Rather than\nsynchronizing with the remote repository in background when there’s\nconnectivity, git requires explicit <code>fetch</code> and <code>push</code> operations to transfer\nbytes over the wire. In this paradigm, it is useful to model the state of the\nremote party at the moment when we talked to them the last time. Theory of mind!</p>\n<p>This <em>hopefully</em> deconfuses git’s concept of local and remote branches. Consider\nthe <code>main</code> branch. It exists on the remote named <code>origin</code> as <code>refs/heads/main</code>.\nWhen you synchronize your local repository with <code>origin</code>, you get\n<code>refs/remotes/origin/main</code> —\nyou current best knowledge about the the state of\n<code>main</code> on the <code>origin</code>.</p>\n<p>And then there’s your local <code>refs/heads/main</code>. It typically starts pointing at\nthe same commit as\n<code>refs/remotes/origin/main</code>.\nBut, when you make a commit, <code>refs/heads/main</code> advances, but\n<code>refs/remotes/origin/main</code>\nstays the same.</p>\n<p>When you try to push your local commit to origin, you will get a conflict, if\nthe <code>main</code> branch on the <code>origin</code> advanced in the meanwhile. In that case, git\nautomatically updates\n<code>refs/remotes/origin/main</code>\n(as that’s just a local mirror of the remote state), but then it’s on you to\nupdate <code>refs/heads/main</code> and push it again.</p>\n<p>Revisiting the full example:</p>\n<pre><code>$ git fetch origin master\n$ git switch -c my-feature origin/master</code></pre>\n<p>The first command looks up the URL for the <code>origin</code> remote in <code>.git/config</code> and\nmakes a network request to that machine. As a result, the local\n<code>refs/remotes/origin/master</code> gets updated to the same commit as\n<code>refs/heads/master</code> remotely (the commit and its ancestors are transferred\nlocally as a result).</p>\n<p>The second command creates a <code>refs/heads/my-feature</code> ref (a branch), whose\nstarting point is <code>refs/remotes/origin/master</code>. It is an example of a leaky\nabstraction.</p>\n<p>The second argument there is a (shorthand of a) ref, so you can do</p>\n<pre><code>$ git switch -c my-feature \\\n    refs/remotes/origin/master</code></pre>\n<p>But, although the first argument <em>creates</em> a ref, it isn’t a ref itself. In\nother words, if you try to elaborate it as well</p>\n<pre><code>$ git switch -c refs/heads/my-feature \\\n    refs/remotes/origin/master</code></pre>\n<p>you’ll get</p>\n<pre><code>refs/heads/refs/heads/my-feature</code></pre>\n<p>That’s all! I am pretty sure this isn’t particularly useful, but maybe it is\ninteresting!</p>","headings":[]}}