Move the most recent commit(s) to a new branch with Git

前端 未结 14 1214
梦毁少年i
梦毁少年i 2020-11-22 03:28

I\'d like to move the last several commits I\'ve committed to master to a new branch and take master back to before those commits were made. Unfortunately, my Git-fu is not

相关标签:
14条回答
  • 2020-11-22 04:03

    Moving to an existing branch

    If you want to move your commits to an existing branch, it will look like this:

    git checkout existingbranch
    git merge master
    git checkout master
    git reset --hard HEAD~3 # Go back 3 commits. You *will* lose uncommitted work.
    git checkout existingbranch
    

    Moving to a new branch

    WARNING: This method works because you are creating a new branch with the first command: git branch newbranch. If you want to move commits to an existing branch you need to merge your changes into the existing branch before executing git reset --hard HEAD~3 (see Moving to an existing branch above). If you don't merge your changes first, they will be lost.

    Unless there are other circumstances involved, this can be easily done by branching and rolling back.

    # Note: Any changes not committed will be lost.
    git branch newbranch      # Create a new branch, saving the desired commits
    git reset --hard HEAD~3   # Move master back by 3 commits (Make sure you know how many commits you need to go back)
    git checkout newbranch    # Go to the new branch that still has the desired commits
    

    But do make sure how many commits to go back. Alternatively, you can instead of HEAD~3, simply provide the hash of the commit (or the reference like origin/master) you want to "revert back to" on the master (/current) branch, e.g:

    git reset --hard a1b2c3d4
    

    *1 You will only be "losing" commits from the master branch, but don't worry, you'll have those commits in newbranch!

    WARNING: With Git version 2.0 and later, if you later git rebase the new branch upon the original (master) branch, you may need an explicit --no-fork-point option during the rebase to avoid losing the carried-over commits. Having branch.autosetuprebase always set makes this more likely. See John Mellor's answer for details.

    0 讨论(0)
  • 2020-11-22 04:08

    Yet another way to do this, using just 2 commands. Also keeps your current working tree intact.

    git checkout -b newbranch # switch to a new branch
    git branch -f master HEAD~3 # make master point to some older commit
    

    Old version - before I learned about git branch -f

    git checkout -b newbranch # switch to a new branch
    git push . +HEAD~3:master # make master point to some older commit 
    

    Being able to push to . is a nice trick to know.

    0 讨论(0)
  • 2020-11-22 04:09

    Most previous answers are dangerously wrong!

    Do NOT do this:

    git branch -t newbranch
    git reset --hard HEAD~3
    git checkout newbranch
    

    As the next time you run git rebase (or git pull --rebase) those 3 commits would be silently discarded from newbranch! (see explanation below)

    Instead do this:

    git reset --keep HEAD~3
    git checkout -t -b newbranch
    git cherry-pick ..HEAD@{2}
    
    • First it discards the 3 most recent commits (--keep is like --hard, but safer, as fails rather than throw away uncommitted changes).
    • Then it forks off newbranch.
    • Then it cherry-picks those 3 commits back onto newbranch. Since they're no longer referenced by a branch, it does that by using git's reflog: HEAD@{2} is the commit that HEAD used to refer to 2 operations ago, i.e. before we 1. checked out newbranch and 2. used git reset to discard the 3 commits.

    Warning: the reflog is enabled by default, but if you've manually disabled it (e.g. by using a "bare" git repository), you won't be able to get the 3 commits back after running git reset --keep HEAD~3.

    An alternative that doesn't rely on the reflog is:

    # newbranch will omit the 3 most recent commits.
    git checkout -b newbranch HEAD~3
    git branch --set-upstream-to=oldbranch
    # Cherry-picks the extra commits from oldbranch.
    git cherry-pick ..oldbranch
    # Discards the 3 most recent commits from oldbranch.
    git branch --force oldbranch oldbranch~3
    

    (if you prefer you can write @{-1} - the previously checked out branch - instead of oldbranch).


    Technical explanation

    Why would git rebase discard the 3 commits after the first example? It's because git rebase with no arguments enables the --fork-point option by default, which uses the local reflog to try to be robust against the upstream branch being force-pushed.

    Suppose you branched off origin/master when it contained commits M1, M2, M3, then made three commits yourself:

    M1--M2--M3  <-- origin/master
             \
              T1--T2--T3  <-- topic
    

    but then someone rewrites history by force-pushing origin/master to remove M2:

    M1--M3'  <-- origin/master
     \
      M2--M3--T1--T2--T3  <-- topic
    

    Using your local reflog, git rebase can see that you forked from an earlier incarnation of the origin/master branch, and hence that the M2 and M3 commits are not really part of your topic branch. Hence it reasonably assumes that since M2 was removed from the upstream branch, you no longer want it in your topic branch either once the topic branch is rebased:

    M1--M3'  <-- origin/master
         \
          T1'--T2'--T3'  <-- topic (rebased)
    

    This behavior makes sense, and is generally the right thing to do when rebasing.

    So the reason that the following commands fail:

    git branch -t newbranch
    git reset --hard HEAD~3
    git checkout newbranch
    

    is because they leave the reflog in the wrong state. Git sees newbranch as having forked off the upstream branch at a revision that includes the 3 commits, then the reset --hard rewrites the upstream's history to remove the commits, and so next time you run git rebase it discards them like any other commit that has been removed from the upstream.

    But in this particular case we want those 3 commits to be considered as part of the topic branch. To achieve that, we need to fork off the upstream at the earlier revision that doesn't include the 3 commits. That's what my suggested solutions do, hence they both leave the reflog in the correct state.

    For more details, see the definition of --fork-point in the git rebase and git merge-base docs.

    0 讨论(0)
  • 2020-11-22 04:12

    For those wondering why it works (as I was at first):

    You want to go back to C, and move D and E to the new branch. Here's what it looks like at first:

    A-B-C-D-E (HEAD)
            ↑
          master
    

    After git branch newBranch:

        newBranch
            ↓
    A-B-C-D-E (HEAD)
            ↑
          master
    

    After git reset --hard HEAD~2:

        newBranch
            ↓
    A-B-C-D-E (HEAD)
        ↑
      master
    

    Since a branch is just a pointer, master pointed to the last commit. When you made newBranch, you simply made a new pointer to the last commit. Then using git reset you moved the master pointer back two commits. But since you didn't move newBranch, it still points to the commit it originally did.

    0 讨论(0)
  • 2020-11-22 04:12

    Simplest way to do this:

    1. Rename master branch to your newbranch (assuming you are on master branch):

    git branch -m newbranch
    

    2. Create master branch from the commit that you wish:

    git checkout -b master <seven_char_commit_id>
    

    e.g. git checkout -b master a34bc22

    0 讨论(0)
  • 2020-11-22 04:13

    To do this without rewriting history (i.e. if you've already pushed the commits):

    git checkout master
    git revert <commitID(s)>
    git checkout -b new-branch
    git cherry-pick <commitID(s)>
    

    Both branches can then be pushed without force!

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