I've used git replace --graft
to record that a version was actually a (manually performed) merge between two versions:
git replace --graft <merged-version> <predecessor-version> <version-merged-from>
That made a change to my (local, private) repository.
I now want to make that change available to other members of my team, by "pushing" it to our shared repository (on Github, it so happens). How do I do that? A simple git push
seems to have no effect.
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).
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)
Be careful when using git replace --graft
: Git 2.22 (Q2 2019) fixes a bug where, when given a tag that points at a commit-ish, "git replace --graft
" failed to peel the tag before writing a replace ref, which did not make sense because the old graft mechanism the feature wants to mimick only allowed to replace one commit object with another.
See commit ee521ec, commit f8e44a8, commit 5876170, commit 502d87b (31 Mar 2019) by Christian Couder (chriscool
).
(Merged by Junio C Hamano -- gitster
-- in commit ce2a18f, 08 May 2019)
replace
: peel tag when passing a tag first to--graft
When passing a tag as the first argument to
git replace --graft
, it can be useful to accept it and use the underlying commit as a the commit that will be replaced.This already works for lightweight tags, but unfortunately for annotated tags we have been using the hash of the tag object instead of the hash of the underlying commit.
Especially we would pass the hash of the tag object to
replace_object_oid()
where we would likely fail with an error like:"error: Objects must be of the same type. 'annotated_replaced_object' points to a replaced object of type 'tag' while 'replacement' points to a replacement object of type 'commit'."
This patch fixes that by using the hash of the underlying commit when an annotated tag is passed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With