How to push a "git replace --graft"
Asked Answered
T

3

22

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.

Theft answered 27/2, 2017 at 15:23 Comment(0)
S
25

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.

Schaab answered 27/2, 2017 at 19:31 Comment(2)
I note that you need to use git pull origin 'refs/replace/*:refs/replace/*' to retrieve the grafts. I have a strange situation where someone else has pushed some grafts as well and a pull from a clean repository is for some reason considered a merge. Is there another kind of graft/replacement which is pulled automatically that might cause this behaviour?Slipstream
@BruceAdams: You would want to git fetch these (as I said in the first paragraph), not git pull them, since git pull means "do a fetch, then do a merge or rebase" and it rarely is good to merge or rebase using the retrieved replacements. In any case, no, this is not automatic—but you can add a fetch setting to the per-repo-per-remote config. (It's a bit unwise to set this with --global for reasons that do not fit well in a comment :-) ) I'll add the fetch command to the answer though.Schaab
A
6

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 Github (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)

Apnea answered 29/7, 2021 at 21:34 Comment(0)
S
2

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.

Sirrah answered 11/5, 2019 at 22:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.