How do I swap the order of two parents of a Git commit?

后端 未结 5 897
刺人心
刺人心 2020-12-13 04:38

A merge commit is a commit with at least two parents. These parents are in specific order.

If I\'m currently on the branch master, and I merge in the br

相关标签:
5条回答
  • 2020-12-13 05:09

    You may want to use git replace --graft (ex. git replace --graft HEAD HEAD^2 HEAD^1) instead of rewriting history. It creates a replacement commit and keeps its children unchanged.

    Edit: Created replacements are not shared by default. You need to configure remotes like push = refs/replace/*:refs/replace/* and also tell your contributors to set fetch = refs/replace/*:refs/replace/*.

    See also:

    • How do git grafts and replace differ? (Are grafts now deprecated?)
    • How to push 'refs/replace' without pushing any other refs in git?
    0 讨论(0)
  • 2020-12-13 05:11

    One way would be to simulate a merge. So how can we do that?

    Lets assume you have the something like following commit graph:

    * (master) Merge branch 'feature'
    |\
    | * (feature) feature commit
    * | master commit
    . .
    . .
    . .
    

    Keep the changes

    We want to merge master into feature but we want to keep the changes, so at first we switch to master, from which we "manually" update our HEAD reference to point at feature while not changing the working tree.

    git checkout master
    git symbolic-ref HEAD refs/heads/feature
    

    The symbolic-ref command is similar to git checkout feature but doesn't touch the working tree. So all changes from master remain.

    Undo the old merge

    Now we have all changes from the merge in the working tree. So we continue with "undoing" the merge by resetting master. If you don't feel comfortable loosing the reference onto the merge commit you can create a temporary tag or branch.

    (If you want to keep the commit message, now is a good time to copy it somewhere save.)

    # Optional
    git tag tmp master
    
    git branch -f master master^
    

    Now your commit tree should look just like before the merge.

    Fake the merge

    And here comes the hacky part. We want to trick git into believing that we are currently merging. We can achieve this by manually creating a MERGE_HEAD file in the .git folder containing the hash of the commit we want to merge.
    So we do this:

    git rev-parse master > .git/MERGE_HEAD
    

    If you are using a git bash, git will now tell you that it is currently in the process of merging.

    To finish our merge we just have to commit.

    git commit
    # Enter your commit message
    

    And it's done. We recreated our merge commit but with swapped parents. So you commit history should now look like this:

    * (feature) Merge branch 'master'
    |\
    | * (master) master commit
    * | feature commit
    . .
    . .
    . .
    

    If you need any further information don't hesitate to ask.

    0 讨论(0)
  • 2020-12-13 05:15

    Actualy, there's a really cool command I learned recently that will do exactly what you want:

    git commit-tree -p HEAD^2 -p HEAD^1 -m "Commit message" "HEAD^{tree}" 
    

    This will create a new commit based on what is currently HEAD, but pretend that it's parents were HEAD^2,HEAD^1 (note this is the reversed order).

    git-commit-tree prints the new revision as output, so you might combine it with a git-reset-hard:

    git reset --hard $(git commit-tree -p HEAD^2 -p HEAD^1 -m "New commit message" "HEAD^{tree}")
    
    0 讨论(0)
  • 2020-12-13 05:15

    Update - It's never that easy, is it?

    I've recognized a flaw in the original instructions I suggested: when doing a checkout with path arguments, you might expect a file to be removed from your working path because it isn't in the commit you're checking out; but then you might be surprised...


    As with any history rewrite, it's worth noting that you probably shouldn't do this (using any of the methods form any of these answers, including this one) to any merge you've already pushed. That said...

    The previous answers are fine, but if you'd like to avoid using (or needing to know) plumbing commands or other git inner workings - and if that's more important to you than having a one-liner like Jared's solution - then here's an alternative:

    The overall structure of this solution is similar to zeeker's, but where he uses plumbing commands to manipulate HEAD or "tricks" git into completing a merge, we'll just use porcelain.

    So as before we have

    * (master) Merge Commit
    | \
    | * (feature) fixed something
    * | older commit on master
    

    Let's begin:

    1) Tag the old merge

    We'll actually be using this tag. (If you want to write down the abbreviated SHA1 instead, that'll work; but the point here is making the fix user friendly, so...)

    git tag badmerge master
    

    Now we have

    * (master) [badmerge] Merge Commit
    | \
    | * (feature) fixed something
    * | older commit on master
    

    2) Take the merge out of master's history

    git branch -f master master^
    

    Simple enough:

    * [badmerge] Merge Commit
    | \
    | * (feature) fixed something
    * | (master) older commit on master
    

    3) Start a new merge

    git checkout feature
    

    If we just run git merge master now, we know the merge will fail; but we don't want to redo the manual conflict resolution. If we had a way to overlay the data from badmerge onto our new merge commit, we'd be all set... and we do!

    To start the merge (a) without creating conflict state to be cleaned up, but (b) leaving the merge commit open so we can "fix" it:

    git merge --no-commit --strategy=ours master
    

    4) Overlay the already-resolved/merged data from badmerge

    Making sure we're in the root directory of the repo we might then do git checkout badmerge -- . (note that we've provided a path (.) to git checkout, so it only updates the working tree and the index); but this actually can be a problem.

    If the merge resulted in files being deleted (but they're in our working tree right now), then the above command won't get rid of them. So if we're not sure, we have a couple options to be safe from that possibility:

    We could first clear our working tree... but we do need to be careful about not wiping out the .git folder, and may need special handling for anything in our ignore file.

    In the simple case - .git is the only .-file, nothing being ignored - we could do

    rm -rf *
    git checkout badmerge -- .
    

    Or, if that seems too risky, another approach is to skip the rm, do the git checkout badmerge -- ., and then diff against badmerge to see if anything needs cleaning up.

    5) Complete the merge

    git commit
    git tag -d badmerge
    
    0 讨论(0)
  • 2020-12-13 05:21

    Inspired by this answer, I came up with this:

    git replace -g HEAD HEAD^2 HEAD^1 && 
    git commit --amend && 
    git replace -d HEAD@{1}
    

    The first commands switches the two parents in something called a replacement ref, but only stores it locally, and people have called it a hack.

    The second command creates a new commit.

    The third command deletes the older replacement ref, so it doesn't mess up the other commits depending on that commit.

    0 讨论(0)
提交回复
热议问题