{"thread":{"id":"65686","subject":"How does git track history overwrites?","startedAt":"2026-05-24T23:43:42Z","lastAt":"2026-05-25T23:20:25Z","messageCount":6,"participants":["Jens Tröger","Chris Torek","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"544016","messageId":"089615C1-6526-4ADC-926A-6A232F330DA2@light-speed.de","threadId":"65686","inReplyTo":null,"subject":"How does git track history overwrites?","fromName":"Jens Tröger","fromEmail":"jens.troeger@light-speed.de","sentAt":"2026-05-24T23:41:50Z","receivedAt":"2026-05-24T23:43:42Z","isPatch":false,"body":"Hello,\n\nI’m looking for details and some clarification on a `git fetch` behavior I observed, but can’t quite explain. More context is in this Github comment:\n\n  https://github.com/jenstroeger/python-package-template/pull/1190#discussion_r3288253713\n\nbut it boils down to this:\n\n  /tmp/bla > git -c protocol.version=2 fetch origin dda8db18cfc68df532abf33b185ecd12d5b7b326 --depth=1\n\nIt seems that sha dda8db1 (tag 1.20.0 previously pointed at it) was replaced due to a suspected history overwrite with fda7769 (tag 1.20.0 now points at it) and git figures that out:\n\n  ...\n\n  From https://github.com/adamchainz/blacken-docs\n  * branch dda8db18cfc68df532abf33b185ecd12d5b7b326 -> FETCH_HEAD\n\nAnd then:\n\n  /tmp/bla > git checkout FETCH_HEAD\n  Note: switching to 'FETCH_HEAD’\n\n  ...\n\n  HEAD is now at fda7769 Version 1.20.0\n\nAnd:\n\n  /tmp/bla > cat .git/HEAD \n  fda77690955e9b63c6687d8806bafd56a526e45f\n  /tmp/bla > cat .git/FETCH_HEAD \n  dda8db18cfc68df532abf33b185ecd12d5b7b326 'dda8db18cfc68df532abf33b185ecd12d5b7b326' of https://github.com/adamchainz/blacken-docs\n\nI’d like to understand the details some more, and how I could manually make that connection?\n\nThank you!\nJens\n\n"},{"id":"544029","messageId":"CAPx1Gvco73KBWpP=h3r+Y3QQAYnFKoooBRjTeDGxEmFfm0JcmQ@mail.gmail.com","threadId":"65686","inReplyTo":"089615C1-6526-4ADC-926A-6A232F330DA2@light-speed.de","subject":"Re: How does git track history overwrites?","fromName":"Chris Torek","fromEmail":"chris.torek@gmail.com","sentAt":"2026-05-25T03:46:11Z","receivedAt":"2026-05-25T03:46:25Z","isPatch":false,"body":"On Sun, May 24, 2026 at 4:44 PM Jens Tröger <jens.troeger@light-speed.de> wrote:\n> I’m looking for details and some clarification on a `git fetch` behavior I observed, but can’t quite explain. ...\n\nThis isn't really specific to \"git fetch\" at all, except for the\nusage of FETCH_HEAD.\n\nTo really understand this properly, we need to understand\nthe root of a seeming contradiction:\n\n1. Once saved in Git, no commit (in fact, no internal object of any sort)\n   can ever be changed.\n2. And yet, \"git rebase\" and force-push operations seem to rewrite\n   history.\n\nHow can commits be immutable and yet rewrite-able? The trick here\nlies in how we (humans) *find* commits.\n\nInside a Git repository, the \"true name\" of any commit (or indeed\nany internal object) is its raw hash ID, such as your example of\ndda8db18cfc68df532abf33b185ecd12d5b7b326. The hash ID (or\n\"object ID\", though right now there are only two forms, a SHA1\nhash or a SHA256 hash) is specific to that one object once it is\ncreated, and forever more can never be used for any other object.\nIt will always mean that original object, as long as that object\nexists.\n\nThus, as long as that commit exists, it's *that* commit, with *that*\nID, and no other.\n\nBut we (humans) don't *use* hash IDs. They're too cumbersome.\nSo Git provides us with the ability to translate a name to an ID:\n\n> It seems that sha dda8db1 (tag 1.20.0 previously pointed at it)\n\nThe *name* refs/tags/1.20.0 used to produce the above ID.\n\n> was replaced ... with fda7769 (tag 1.20.0 now points at it)\n\nSome human directed Git to forcibly replace the hash ID associated\nwith the tag, in some repository or repositories.\n\n(As the manuals note, this kind of forcible replacement of tags is\noften a bad idea. It's usually better, once the tag has escaped the\nconfinement of a single repository anyway, to just admit that you\ngoofed up and make a new tag.)\n\nIf you use raw hash IDs, you can never be bitten by this kind of\ntag replacement, but of course that's a bad idea for different\n(and presumably obvious) reasons. I couldn't possibly name the\nhash ID without using cut-and-paste here. I can *type* \"1.20.0\"\nrepeatedly without error though.\n\n(There are additional considerations, having to do with how Git\ncleans up unwanted leftover junk, via git gc / git maintenance. In\nparticular Git uses the human-readable names to figure out which\nobjects are useful, and which are unwanted junk. So you have to\nidentify *some* commits with names, or they'll eventually get\ngarbage-collected.)\n\n[At this point, you ran git fetch with a raw hash ID, and:]\n\n>   From https://github.com/adamchainz/blacken-docs\n>   * branch dda8db18cfc68df532abf33b185ecd12d5b7b326 -> FETCH_HEAD\n\nWhen git fetch obtains something from another different Git repository,\nthe new things have the same IDs in both repositories. Normally we do\nthis by *name* (branch or tag name), but for historical reasons, the fetch\noperation deposits a hash ID (often along with additional information)\n in the file `.git/FETCH_HEAD`. This file then works as a pseudo-name\nfor the branch, tag, or commit(s) thus obtained:\n\n> And then:\n>\n>   /tmp/bla > git checkout FETCH_HEAD\n>   Note: switching to 'FETCH_HEAD’\n\nThis gives you a \"detached HEAD\" state, using the hash ID stored in\n.git/FETCH_HEAD. That hash ID will be overwritten (thus lost) by the\n*next* git fetch, so you're expected to save it in some more-permanent\nname if you want it to stick around.\n\nThe key difference between a branch name and a tag name is that\nbranch names are *expected* to map to different hash IDs over time,\nwith updates adding new commits to the branch causing the branch\nname to remember the latest commit's ID. Each commit in turn\nremembers the IDs of its parent commit or commits, so knowing\nthe *last* one suffices to allow Git to find *every* one.\n\nRewriting history with rebase consists of copying old (presumably\nbad) commits to new (presumably good/better) ones, whose backwards\nlinks to each previous commit chain through the new-and-improved\ncommits until you reach the point where the rewrite joins existing\nhistory. Then we update the branch name to remember the latest\nof the new-and-improved commits, and it *seems* that we've changed\nhistory. The old history is still in there, and will stick around for quite\na while (at least a month by default, in standard clones) \"just in case\".\n\nTag names are not supposed to move, and whether someone else's tag\nupdate to their clone changes your own clone's tags is something\nyou can control to some extent. It's not a good idea to depend on\nother people's clones to follow tag changes, but it's also not a\ngood idea to depend on your own or other people's clones *not* to\nfollow such changes, since both behaviors are possible.\n\nChris\n"},{"id":"544035","messageId":"87se7gasn8.fsf@gitster.g","threadId":"65686","inReplyTo":"089615C1-6526-4ADC-926A-6A232F330DA2@light-speed.de","subject":"Re: How does git track history overwrites?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-25T06:51:39Z","receivedAt":"2026-05-25T06:51:50Z","isPatch":false,"body":"Jens Tröger <jens.troeger@light-speed.de> writes:\n\n> Hello,\n>\n> I’m looking for details and some clarification on a `git fetch` behavior I observed, but can’t quite explain. More context is in this Github comment:\n>\n>   https://github.com/jenstroeger/python-package-template/pull/1190#discussion_r3288253713\n>\n> but it boils down to this:\n>\n>   /tmp/bla > git -c protocol.version=2 fetch origin dda8db18cfc68df532abf33b185ecd12d5b7b326 --depth=1\n>\n> It seems that sha dda8db1 (tag 1.20.0 previously pointed at it) was replaced due to a suspected history overwrite with fda7769 (tag 1.20.0 now points at it) and git figures that out:\n>\n>   ...\n>\n>   From https://github.com/adamchainz/blacken-docs\n>   * branch dda8db18cfc68df532abf33b185ecd12d5b7b326 -> FETCH_HEAD\n>\n> And then:\n>\n>   /tmp/bla > git checkout FETCH_HEAD\n>   Note: switching to 'FETCH_HEAD’\n>\n>   ...\n>\n>   HEAD is now at fda7769 Version 1.20.0\n>\n> And:\n>\n>   /tmp/bla > cat .git/HEAD \n>   fda77690955e9b63c6687d8806bafd56a526e45f\n>   /tmp/bla > cat .git/FETCH_HEAD \n>   dda8db18cfc68df532abf33b185ecd12d5b7b326 'dda8db18cfc68df532abf33b185ecd12d5b7b326' of https://github.com/adamchainz/blacken-docs\n>\n> I’d like to understand the details some more, and how I could manually make that connection?\n\nWhere does this line in your discussion page at GitHub (which is\nomitted from the post to this list) come from?\n\n    commit fda77690955e9b63c6687d8806bafd56a526e45f (grafted, HEAD)\n\nAre you doing anything funky with .git/info/grafts by any chance?\n"},{"id":"544081","messageId":"074E783A-027D-4C5B-BC44-CC38C53735D7@light-speed.de","threadId":"65686","inReplyTo":"87se7gasn8.fsf@gitster.g","subject":"Re: How does git track history overwrites?","fromName":"Jens Tröger","fromEmail":"jens.troeger@light-speed.de","sentAt":"2026-05-25T22:47:10Z","receivedAt":"2026-05-25T22:47:29Z","isPatch":false,"body":"Thank you Chris and Junio!\n\n\n> [Junio] Where does this line in your discussion page at GitHub (which is\n> omitted from the post to this list) come from?\n> \n>    commit fda77690955e9b63c6687d8806bafd56a526e45f (grafted, HEAD)\n> \n> Are you doing anything funky with .git/info/grafts by any chance?\n\nThat line is the result of a `git log` after the `git fetch` I mentioned in my initial email.\n\n\n> [Chris] To really understand this properly, we need to understand\n> the root of a seeming contradiction:\n> \n> [...]\n\nThank you for your elaborate explanation, Chris, that all makes a lot of sense. A few follow-up questions:\n\n •  Is all the object information stored with a repo clone locally as well, or does some/most/all of it stay on the remote server repo?\n •  How exactly does git connect the dots between commit dda8db1 and fda7769, how does it “know” the former was superseded by the latter (i.e. I fetch the former and Git uses the latter for head)?\n •  Based on the previous question, can I manually find such a connection between two commit objects too?\n\nIt sounds like reading some internals would be helpful. I’m noodling through https://git-scm.com/book/en/v2/Git-Internals-Git-Objects but perhaps you have some more recommendations?\n\nWith many greetings,\nJens\n\n"},{"id":"544084","messageId":"xmqqfr3fnl2n.fsf@gitster.g","threadId":"65686","inReplyTo":"074E783A-027D-4C5B-BC44-CC38C53735D7@light-speed.de","subject":"Re: How does git track history overwrites?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-25T23:09:04Z","receivedAt":"2026-05-25T23:09:06Z","isPatch":false,"body":"Jens Tröger <jens.troeger@light-speed.de> writes:\n\n> Thank you Chris and Junio!\n>\n>\n>> [Junio] Where does this line in your discussion page at GitHub (which is\n>> omitted from the post to this list) come from?\n>> \n>>    commit fda77690955e9b63c6687d8806bafd56a526e45f (grafted, HEAD)\n>> \n>> Are you doing anything funky with .git/info/grafts by any chance?\n>\n> That line is the result of a `git log` after the `git fetch` I mentioned in my initial email.\n\nSorry, I may have been unclear.  I specifically meant the \"grafted,\n\" part in the message.  I know how \"git log\" output looks like ;-)\n\n"},{"id":"544085","messageId":"ahTZN7BurySMbEgM@light-speed.de","threadId":"65686","inReplyTo":"xmqqfr3fnl2n.fsf@gitster.g","subject":"Re: How does git track history overwrites?","fromName":"Jens Tröger","fromEmail":"jens.troeger@light-speed.de","sentAt":"2026-05-25T23:20:23Z","receivedAt":"2026-05-25T23:20:25Z","isPatch":false,"body":"Hello Junio,\n\n> Sorry, I may have been unclear.  I specifically meant the \"grafted,\n> \" part in the message.  I know how \"git log\" output looks like ;-)\n\nThe “grafted” too was part of the git log output; here is the complete\ncmd line output:\n\n    /tmp/bla > git log\n    commit fda77690955e9b63c6687d8806bafd56a526e45f (grafted, HEAD)\n    Author: Adam Johnson <me@adamj.eu>\n    Date:   Mon Sep 8 16:31:35 2025 +0100\n\n        Version 1.20.0\n\nOn *why* the “grafted” is there in the first place, I suspect that’s got\nto do with the fetch --depth=1 and that previous history isn’t available\nin the shallow repo clone.\n\nCheers,\nJens\n"}]}