How to push a "git replace --graft"

Grafts exist inside the refs/replace/ hierarchy. (Or, it might be better to say, "owe their existence to" such references.) To transfer them from one repository to another, then, you must push or fetch such references.

For instance:

git push origin refs/replace/5c714d7798d1dc9c18d194fa6448680515c0ccdb

when commit 5c714d7798d1dc9c18d194fa6448680515c0ccdb has a replacement (in my case the replacement was new commit object ceba978ce6dad3b52d12134f4ef2720c5f3a9002, i.e., Git normally doesn't "see" 5c714d7, looking to replacement object ceba978 instead).

To push all replacements:

git push origin 'refs/replace/*:refs/replace/*'

(the quotes are sometimes needed to keep the shell from mangling the asterisks; exactly when, and which kind of quotes to use, is somewhat shell-dependent, though both single and double quotes work on all Unix-y shells).

Notes on fetching replacements

If some remote R has replacements, and you want to bring all of theirs in to your repository, use git fetch R 'refs/replace/*:refs/replace/*' (or the same with a prefix + if you want their replacements to override any you have already). You can automate this for any given repository and remote. For instance, if you run git config --edit, you will find that your existing origin remote has several settings that look like this:

[remote "origin"]
    url = ...
    fetch = +refs/heads/*:refs/remotes/origin/*

Simply add the line:

    fetch = refs/replace/*:refs/replace/*

or:

    fetch = +refs/replace/*:refs/replace/*

to make your Git bring over their Git's refs/replace/*. (Note: no quotes are needed here as the shell is not going to process this line.) The leading plus sign has the same meaning as usual:1 without it, if you already have some reference, you keep yours and ignore theirs. With the leading plus sign, you discard yours and use theirs instead. As with tags, if your reference and their reference already match, it does not matter whether you keep yours or replace yours with theirs; this only matters when you have different ideas about what object some reference should name.


1In fact, the "usual meaning" for leading plus sign depends on whether the reference is supposed to move, such a branch names, or not supposed to move, such as a tag name. The plus mark sets the force flag, i.e., "always take the proposed new setting", but for branch names—which are expected to "move forward"—an update is allowed without force if and only if it is a "forward" (or "fast forward") move. Git originally applied this rule to other references like tags as well, but the Git folks fixed it in Git 1.8.2. It's not clear to me which rules Git applies to refs/replace/ references, which are not supposed to move, but are not treated extra-specially the way tags are.


For sake of Completeness: git replacements are "virtual", not permanent. The original version of the manipulated commit is still there — it is just shadowed by the replacement commit. The accepted answer describes how to publish those "virtual replacements" also into a shared repository, and how to arrange to get such replacements when fetching. Usually this is the right thing to do.

However, sometimes we want to make such a history fix permanent. With Git, the only way to do this is to synthesise a new history. This can be done with git filter-branch (brittle, low-level) or the very nice tool git-filter-repo on Gitub (officially recommended by the Git project).

Note however, there is no way to force the other users of a shared repository into using a rewritten history. You need to ask them to switch over, e.g. by resetting their master branch or by switching to another new branch. Thus, in a public setup, permanently rewriting history is not feasible; however with a closed user group, e.g. in a commercial setup, this is very much a valid option (and might indeed become necessary to remove some sensible content like credentials)

Tags:

Git

Git Push