Search code examples
git

Is there a way to duplicate a Git commit with different parents without first replacing?


Consider the following sequence of commands:

git replace --graft <sha> <new_parent_sha>
new_commit_sha="$(git show-ref refs/replace/<sha> | awk '{print $1}')"
git update-ref -d refs/replace/<sha>

The end result is that we have a commit (new_commit_sha) with new parents, that is identical to the original commit (same tree, same message, same author and committer, author date, commit date) except that the parents are different.

Is there a more direct way to achieve the same effect of duplicating a commit with replaced parents without first creating a replace ref and then deleting it?

I looked at git-commit-tree but did not see an option to take most commit properties from a source commit.

I also played with git cherry-pick but that tries to apply a patch which can run into conflicts and results in a different tree if the parent is different. I am trying to produce a commit with exactly the same tree (and other properties).


Solution

  • There's an easier way to get the replace ref's sha, and a more direct way to do the whole thing but it's more verbose.

    Easier:

    git replace --graft $sha $parent1 $parent2 $etc
    newcommit=$(git rev-parse refs/replace/$sha)
    git replace -d $sha
    

    More direct, but you have to say the exact alterations you want, the idea is Git's commit format is intentionally straightforward plain text, edit it to replace the parents then write the result as a new commit:

    newcommit=$(
    git cat-file commit $sha | sed -f $youredits | git hash-object -w -t commit --stdin
    )
    

    so for instance the above with two parents and without bothering with the replace-ref tango on a GNU system is

    newcommit=$(
    exec {sedpgm}<<EOD
    1,/^ *$/ { /^parent/d; }   # delete existing parents in the header; add new ones:
    1aparent $parent1
    1aparent $parent2
    EOD
    git cat-file commit $sha | sed -f /dev/fd/$sedpgm | git hash-object -w -t commit --stdin
    #exec {sedpgm}<&- # not needed in a subshell that's going to exit anyway
    )
    

    If you don't have a GNU sed or shell you get to construct the edit instructions to suit your own toolset; it's easier, likely even a bit cheaper, and probably clearer to just do the git replace, it's got the parent-rewiring dance down cold.


    p.s. on a side tangent using more shell readability features, maybe this is getting too deep in the weeds here but if you do have the tools you can get good use of the shell's line-at-a-time readin to make it clearer. (This unfortunately horribly confuses SO's builtin syntax coloring, and even emacs's, but unsurprisingly not vim's.) Markdown eats tabs but the shell can use them to make inline data easier on the eyes, here's what I really used for my smoketest on a two-parent merge here, all leading whitespace is tabs:

    sha=$(git rev-parse @)
    newcommit=$(
    git cat-file commit $sha | sed -f /dev/fd/3 3<<-EOD| git hash-object -w -t commit --stdin
        1,/^ *$/ { /^parent/d; }   # delete existing parents in the header; add new ones:
        1aparent $(git rev-parse @^1)
        1aparent $(git rev-parse @^2)
        EOD
    )
    

    or even

    sha=$(git rev-parse @)
    newcommit=$(
    git cat-file commit $sha | sed -f /dev/fd/3 3<<-EOD \
    | git hash-object -w -t commit --stdin
        1,/^ *$/ { /^parent/d; }   # delete existing parents in the header; add new ones:
        1aparent $(git rev-parse @^1)
        1aparent $(git rev-parse @^2)
        EOD
    )