ChatGPT解决这个技术问题 Extra ChatGPT

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

How do I move my recent commits on master to a new branch, and reset master to before those commits were made? e.g. From this:

master A - B - C - D - E

To this:

newbranch     C - D - E
             /
master A - B 
Note: I asked the opposite question here
eddmann.com/posts/… this one works
Were the comments here purged? I ask because during my bimonthly visit to this question, I always scroll by that comment.
Side-comment: The question is about a very simple case. Reading the answers and all the "don't do this because..." and "a better solution is..." and "warning with version n+..." just after the answers (possibly when it's too late), it seems to me even very simple operations have no straight solutions in git. A graphical version manager where you would just add a tag for the new branch without dealing with what seems to me obscure and archaic syntax would be such a relief. My kingdom and my gold badges to the first one who "forks" git and starts a new approach ;-) it's urgent.
Be sure to read through the first ten answers (or so), as the best are not the most upvoted.

2
25 revs, 23 users 17%

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

You can store uncommitted edits to your stash before doing this, using git stash. Once complete, you can retrieve the stashed uncommitted edits with git stash pop

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 checkout master       # checkout master, this is the place you want to go back
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!

Lastly, you may need to force push your latest changes to main repo:

git push origin master --force

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.


And in particular, don't try to go back further than the point where you last pushed commits to another repository from which somebody else might have pulled.
Wondering if you can explain WHY this works. To me you're creating a new branch, removing 3 commits from the old branch you are still on, and then checking out the branch you made. So how do the commits you removed magically show up in the new branch?
@Jonathan Dumaine: Because I created the new branch before removing the commits from the old branch. They're still there in the new branch.
branches in git are just markers which point to commits in history, there is nothing being cloned, created or deleted (except the markers)
Also note: Don't do this with uncommitted changes in your working copy! This just bit me! :(
R
Ryan Lundy

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.


I also needed to do a git push origin master --force for the change to show up in main repository.
This answer causes commits to be lost: next time you git rebase, the 3 commits will be silently discarded from newbranch. See my answer for details and safer alternatives.
@John, that's nonsense. Rebasing without knowing what you're doing causes commits to be lost. If you lost commits, I'm sorry for you, but this answer didn't lose your commits. Note that origin/master doesn't appear in the above diagram. If you pushed to origin/master and then made the changes above, sure, things would go funny. But that's a "Doctor, it hurts when I do this" kind of problem. And it's out of scope for what the original question asked. I suggest you write your own question to explore your scenario instead of hijacking this one.
@John, in your answer, you said "Don't do this! git branch -t newbranch". Go back and read the answers again. Nobody suggested doing that.
@Kyralessa, sure, but if you look at the diagram in the question, it's clear that they want newbranch to be based off their existing local master branch. After performing the accepted answer, when the user gets around to running git rebase in newbranch, git will remind them that they forgot to set the upstream branch, so they'll run git branch --set-upstream-to=master then git rebase and have the same problem. They may as well use git branch -t newbranch in the first place.
I
Ivan

In General...

The method exposed by sykora is the best option in this case. But sometimes is not the easiest and it's not a general method. For a general method use git cherry-pick:

To achieve what OP wants, its a 2-step process:

Step 1 - Note which commits from master you want on a newbranch

Execute

git checkout master
git log

Note the hashes of (say 3) commits you want on newbranch. Here I shall use:
C commit: 9aa1233
D commit: 453ac3d
E commit: 612ecb3

Note: You can use the first seven characters or the whole commit hash

Step 2 - Put them on the newbranch

git checkout newbranch
git cherry-pick 612ecb3
git cherry-pick 453ac3d
git cherry-pick 9aa1233

OR (on Git 1.7.2+, use ranges)

git checkout newbranch
git cherry-pick 612ecb3~1..9aa1233

git cherry-pick applies those three commits to newbranch.


This works very well if you accidentally commit the wrong, non-master branch, when you should have created a new feature branch.
The information on git cherry-pick is nice, but the commands in this post don't work. 1) the 'git checkout newbranch' should be 'git checkout -b newbranch' since newbranch doesn't already exist; 2) if you checkout newbranch from the existing master branch it ALREADY has those three commits included in it, so there's no use in picking them. At the end of the day to get what the OP wanted, you'll still have to do some form of reset --hard HEAD.
+1 for a useful approach in some situations. This is good if you only want to pull your own commits (which are interspersed with others) into a new branch.
It's better answer. This way you can move commits to any branch.
I was not able to use the cherry-pick range, got "error: could not apply c682e4b..." on one of the commits, but when used cherry-pick on each commit one at a time, it worked fine (I had to do git cherry-pick --abort after the error). Commenting because I thought those would be equivalent. This is on git version 2.3.8 (Apple Git-58)
J
John Mellor

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.


This answer says "Do NOT do this!" above something that no one suggested doing.
Most people don't rewrite published history, especially on master. So no, they are not dangerously wrong.
@Kyralessa, the -t you are referring to in git branch happens implicitly if you have git config --global branch.autosetuprebase always set. Even if you don't, I already explained to you that the same problem occurs if you setup tracking after performing these commands, as the OP likely intends to do given their question.
@RockLee, yes, the general the way to fix such situations is to create a fresh branch (newbranch2) from a safe starting point then cherry-pick all the commits you want to keep (from badnewbranch to newbranch2). Cherry-picking will give the commits new hashes, so you'll be able to safely rebase newbranch2 (and can now delete badnewbranch).
Too bad this isn't the accepted answer. I followed the steps in the accepted answer and lost 6 commits, just as you described!
a
aragaer

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.


Current directory. I guess this would work only if you are in a top directory.
The local push is grin-inducing, but on reflection, how is it different to git branch -f here?
@GerardSexton . is current director. git can push to REMOTES or GIT URLs. path to local directory is supported Git URLs syntax. See the GIT URLS section in git help clone.
I don't know why this is not rated higher. Dead simple, and without the small but potential danger of git reset --hard.
@Godsmith My guess is people prefer three simple commands to two slightly more obscure commands. Also, top voted answers get more upvotes by nature of being displayed first.
S
Slam

Much simpler solution using git stash

Here's a far simpler solution for commits to the wrong branch. Starting on branch master that has three mistaken commits:

git reset HEAD~3
git stash
git checkout newbranch
git stash pop

When to use this?

If your primary purpose is to roll back master

You want to keep file changes

You don't care about the messages on the mistaken commits

You haven't pushed yet

You want this to be easy to memorize

You don't want complications like temporary/new branches, finding and copying commit hashes, and other headaches

What this does, by line number

Undoes the last three commits (and their messages) to master, yet leaves all working files intact Stashes away all the working file changes, making the master working tree exactly equal to the HEAD~3 state Switches to an existing branch newbranch Applies the stashed changes to your working directory and clears the stash

You can now use git add and git commit as you normally would. All new commits will be added to newbranch.

What this doesn't do

It doesn't leave random temporary branches cluttering your tree

It doesn't preserve the mistaken commit messages, so you'll need to add a new commit message to this new commit

Update! Use up-arrow to scroll through your command buffer to reapply the prior commit with its commit message (thanks @ARK)

Goals

The OP stated the goal was to "take master back to before those commits were made" without losing changes and this solution does that.

I do this at least once a week when I accidentally make new commits to master instead of develop. Usually I have only one commit to rollback in which case using git reset HEAD^ on line 1 is a simpler way to rollback just one commit.

Don't do this if you pushed master's changes upstream

Someone else may have pulled those changes. If you are only rewriting your local master there's no impact when it's pushed upstream, but pushing a rewritten history to collaborators can cause headaches.


Thanks, am so glad I read past/through so much to get to here, cause it's a pretty common use case for me as well. Are we so atypical?
I think we're totally typical and "oops I commited to master by mistake" is the most common use-case for need to revert a handful or less of commits. Lucky this solution is so simple I have it memorized now.
This should be the accepted answer. It's straightforward, easy to understand and easy to remember
You can easily get your commit messages back, too, if you happen have them in your CLI (command line) history. I happened to have both the git add and git commit commands that I used so all I had to do was hit up arrow and enter a few times and boom! Everything was back, but on the right branch now.
If "newbranch" didn't exist yet, then you can move the unstaged changes after reset to a new branch with this command w/o having to stash: git checkout -b newbranch
h
hamdiakoguz

This doesn't "move" them in the technical sense but it has the same effect:

A--B--C  (branch-foo)
 \    ^-- I wanted them here!
  \
   D--E--F--G  (branch-bar)
      ^--^--^-- Opps wrong branch!

While on branch-bar:
$ git reset --hard D # remember the SHAs for E, F, G (or E and G for a range)

A--B--C  (branch-foo)
 \
  \
   D-(E--F--G) detached
   ^-- (branch-bar)

Switch to branch-foo
$ git cherry-pick E..G

A--B--C--E'--F'--G' (branch-foo)
 \   E--F--G detached (This can be ignored)
  \ /
   D--H--I (branch-bar)

Now you won't need to worry about the detached branch because it is basically
like they are in the trash can waiting for the day it gets garbage collected.
Eventually some time in the far future it will look like:

A--B--C--E'--F'--G'--L--M--N--... (branch-foo)
 \
  \
   D--H--I--J--K--.... (branch-bar)

Yes you could alternatively use rebase on the detached branch in the scenario above.
t
teh_senaus

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!


But then you have to deal with the revert scenario, which, depending on your circumstance, can be a lot trickier. If you revert a commit on the branch, Git will still see those commits as have taken place, so in order to undo that, you have to revert the revert. This burns quite a few people, especially when they revert a merge and try to merge the branch back, only to find that Git believes that it's already merged that branch in (which is entirely true).
That's why I cherry-pick the commits at the end, onto a new branch. That way git sees them as new commits, which solves your issue.
This is more dangerous than it first seems, since you're changing the state of the repository's history without really understanding the implications of this state.
I don't follow your argument - the point of this answer is that you're not changing history, simply adding new commits (which effectively undo the redo the changes). These new commits can be pushed and merged as normal.
S
Saikat

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

NOTE: The upstream for newbranch would be origin/master.


Love this solution, because you do not have to rewrite the git commit title/description.
Doesn't this mess up the remote upstream branches? Isn't newbranch now pointing to origin/master?
D
Darkglow

Had just this situation:

Branch one: A B C D E F     J   L M  
                       \ (Merge)
Branch two:             G I   K     N

I performed:

git branch newbranch 
git reset --hard HEAD~8 
git checkout newbranch

I expected that commit I would be the HEAD, but commit L is it now...

To be sure to land on the right spot in the history its easier to work with the hash of the commit

git branch newbranch 
git reset --hard #########
git checkout newbranch

I
Ivan

How can I go from this

A - B - C - D - E 
                |
                master

to this?

A - B - C - D - E 
    |           |
    master      newbranch

With two commands

git branch -m master newbranch

giving

A - B - C - D - E 
                |
                newbranch

and

git branch master B

giving

A - B - C - D - E
    |           |
    master      newbranch

Yep, this works and is quite easy. Sourcetree GUI is a little confused about the changes made in the git shell, but after a fetch it's all right again.
Yes, they are as in the question. The first couple of diagrams are intended to be equivalent to those in the question, just redrawn the way I would like for the purpose of illustration in the answer. Basically rename the master branch as newbranch and create a new master branch where you want it.
S
Shamsul Arefin Sajib

If you just need to move all your unpushed commits to a new branch, then you just need to,

create a new branch from the current one :git branch new-branch-name push your new branch: git push origin new-branch-name revert your old(current) branch to the last pushed/stable state: git reset --hard origin/old-branch-name

Some people also have other upstreams rather than origin, they should use appropriate upstream


p
pankaj

You can do this is just 3 simple step that i used.

1) make new branch where you want to commit you recent update.

git branch <branch name>

2) Find Recent Commit Id for commit on new branch.

git log

3) Copy that commit id note that Most Recent commit list take place on top. so you can find your commit. you also find this via message.

git cherry-pick d34bcef232f6c...

you can also provide some rang of commit id.

git cherry-pick d34bcef...86d2aec

Now your job done. If you picked correct id and correct branch then you will success. So before do this be careful. else another problem can occur.

Now you can push your code

git push


Q
Quesofat

Most of the solutions here count the amount of commits you'd like to go back. I think this is an error prone methodology. Counting would require recounting.

You can simply pass the commit hash of the commit you want to be at HEAD or in other words, the commit you'd like to be the last commit via:

(Notice see commit hash)

To avoid this:

1) git checkout master

2) git branch <feature branch> master

3) git reset --hard <commit hash>

4) git push -f origin master

m
moestly

TLDR

git checkout branch_to_remove_commits
git reset --hard ${hash_of_new_tip}
git checkout -b branch_to_store_commits
# Move commits (single hash, list of hashes or range ffaa..ffoo) 
git cherry-pick ${commit_hash}
git push --set-upstream origin branch_to_store_commits
# Switch back to last branch
git checkout -
git push -f

For me

git log --pretty=oneline -n ${NUMBER}

works best to identify the commit hashes in question.


r
rashok

1) Create a new branch, which moves all your changes to new_branch.

git checkout -b new_branch

2) Then go back to old branch.

git checkout master

3) Do git rebase

git rebase -i <short-hash-of-B-commit>

4) Then the opened editor contains last 3 commit information.

...
pick <C's hash> C
pick <D's hash> D
pick <E's hash> E
...

5) Change pick to drop in all those 3 commits. Then save and close the editor.

...
drop <C's hash> C
drop <D's hash> D
drop <E's hash> E
...

6) Now last 3 commits are removed from current branch (master). Now push the branch forcefully, with + sign before branch name.

git push origin +master

D
David Chelidze

I was surprised that nobody recommended this way:

git checkout master
git checkout <commit hash from which you want to split>
git checkout -b new_branch
git rebase master
git checkout master
git reset --hard <commit hash you splitted>

to explain:

step we checking out the commit where we want to split then from this commit creating a new branch doing rebase will synchronize new_branch and master. So now we have two same branches with same commits with reset on master, we cleanup last commits after split List item


This way only creates a branch with the first commit that changes from Main/Master, but forgets about the rest.
k
kokoro

Using Emacs' git porcelain Magit, you can do this simply by hitting b s (magit-branch-spinoff). You'll be asked to enter a name for your new branch and once you hit enter, voila.

From the Magit documentation:

This command creates and checks out a new branch starting at and tracking the current branch. That branch in turn is reset to the last commit it shares with its upstream. If the current branch has no upstream or no unpushed commits, then the new branch is created anyway and the previously current branch is not touched. This is useful to create a feature branch after work has already began on the old branch (likely but not necessarily "master").


R
Raphael Fernandes

I got to move 7 commits from one old-branch to a new-branch.

git checkout old-branch     # in the example, master
git reset --hard h4sh       # h4sh is the hash for the commit 
git checkout -b new-branch  
git push origin new-branch

After that, both branches were related to the 7 commits I have done. After git checkout new-branch, I was getting fine git log and git status, but, when accessing the old-branch (git checkout old-branch), I'd got the message "git is behind by 7 commits and can be fast-forwarded". What worked for me to erase this message was the followind:

git checkout old-branch
git status 
> git is behind by 7 commits and can be fast-forwarded
git push origin old-branch -f

After that step, the last 7 commits was referenced only for the new-branch and the previous ones were referenced as old-branch and new-branch in the Bitbucket tree.


b
bunjeeb

If you are a UI person like me and you are using Visual Studio. Then you can do the following: In my case, I want to take the latest commit to another branch.

Right-click on the one (commit) before.

https://i.stack.imgur.com/2tz8w.png

So all commit changes will appear in the Git Changes pane. Now, stash your changes

https://i.stack.imgur.com/UJxJg.png

Go to your targeted branch or create a new one from the bottom right corner.

https://i.stack.imgur.com/LnwZO.png

From "Git Changes" double click on your latest Stash. "Stash details" pane will be opened. Click on "Pop", then resolve conflicts (if exists).

https://i.stack.imgur.com/uEGUU.png

And finally, commit your changes.


c
cs94njw

Taking some ideas from other posts, avoiding anything to do with reset, and being ultra paranoid, my solution is:

git branch # changes are available in new branch git push # upload, you may need to mess with "--set-upstream", e.g. git push --set-upstream https:/// check the new branch is in git via a GUI destroy current directory re-clone from git repository

I'm not proud, but I kept my data ;)