简体   繁体   中英

Move an misplaced commit to a new branch

Setup: I have commited a code change to local and also to remote, but in the wrong branch.

My solution:

  1. Have checked out master and created a branch where the code change should be
  2. Cherry-picked the commit from the wrong branch checkin and commited to new branch.
  3. Check out the wrong branch. Reset to the first correct commit and then force push it again to remove the faulty commit on remote branch.

Question : Is this the way to go? If I google it, I get that people use revert and I can't understand why as it seems more complicated and more dangerous.. Why should I use revert?

When you revert some commit you create commit that negates target commit. Result as with reset but you do not have to force push , you can do simple push becouse you add to history not remove from it. Another difference you can revert commit that is not last in history. You can't use reset in this case becouse it leads to loosing all commits since target commit.

Also you may look this question: What's the difference between Git Revert, Checkout and Reset?

Instead of 3 I'd suggest rebase onto :

git rebase --onto COMMIT_BEFORE_WRONG WRONG_COMMIT branch_with_wrong_commit
git push --force-with-lease

This "cuts" the wrong commit.

When adding -i (for interactive ) you can check that the right commits are moved.


Same result would come with:

git rebase -i COMMIT_BEFORE_WRONG

and then changing the word pick to drop in the first line of the "todo" file presented.

I [see] that people use revert and I can't understand why as it seems more complicated and more dangerous.. Why should I use revert?

Should is a strong word. (Not quite as strong as shall or must , but at least fairly strong. :-) )

... Reset to the first correct commit and then force push it again to remove the faulty commit on remote branch

Whenever you have to use git push --force or equivalent, you are moving a branch name in a way that other people might not expect. ( Might is weaker than should: in a partial ordering, I would say may < might < should < shall / must .) In particular, Git branch names naturally move in a progression in which commits get added to branches, because of the way that branches grow by making commits.

Consider:

$ git checkout <somebranch>
... work ...
$ git add <files>  # or --all or . or whatever
$ git commit

The git checkout step has the effect of attaching HEAD to a branch and copying that branch's tip commit , as pointed-to by the branch name, into your Git's index and work-tree so that you can work on it:

... <-F  <-G  <-H   <--branch (HEAD)

The branch named branch —the reference whose full name is refs/heads/ branch —stores the raw hash ID of some commit H . Commit H itself stores the raw hash ID of its parent commit G , which stores the raw hash ID of some commit F , and so on. So we say that the name points to the tip commit H , which points to the earlier commit G , and so on.

The git add step updates your index / staging-area so that it is ready for the commit, and the git commit step creates a new commit. The new commit stores, as its parent hash ID, the hash ID of the currently-checked-out commit H . (It stores as its snapshot the frozen snapshot made from the current index / staging-area, of course.) Then, as its final step, git commit writes the new commit's hash ID into the branch name to which HEAD is attached:

... <-F  <-G  <-H  <-I   <--branch (HEAD)

This is how branches grow, one commit at a time, when people make commits. When you merge in a series of commits en-masse, either as a real merge or as a fast-forward not-really-a-merge-at-all operation, the branch acquires new commits as well, maybe many at once and maybe in a nonlinear fashion, but the important thing is that the new commits always lead back to the existing commits:

...--F--G--H--I---M   <-- master (HEAD)
         \       /
          J--K--L   <-- develop

Adding merge commit M to master leaves commits H and I reachable from master , because we can follow the backwards-pointing internal commit-to-commit arrows—rendered here as lines because arrows are just too hard to draw in text now—using the top-row arrow out of M . (The left-and-down arrow from M to L allows us to ride from M to, say, K or J as well. Think Like (a) Git has a nice analogy to the transit system in Portland, though any metropolitan train system is similar.)

But suppose we do this instead:

...--F--G--H--X  <-- master (HEAD)
         \
          J--K   <-- develop

and then realize that, oops, we meant to put commit X on develop . We use any means appropriate to copy X to a new commit, such as cherry-pick or git rebase --onto (both do the same job). Then we use git checkout master; git reset --hard master~1 git checkout master; git reset --hard master~1 to push X out of the way, so that it's no longer on master :

             X
            /
...--F--G--H  <-- master (HEAD)
         \
          J--K--L   <-- develop

(Here L is the copy of X , put where we wanted it.) This kind of branch-name-motion leaves commit X dangling without any way to find it—at least, no way in our repository. But if we already use git push to send commit X somewhere else, some other Git has a name for it. In fact, so do we:

             X   <-- origin/master
            /
...--F--G--H  <-- master (HEAD)
         \
          J--K--L   <-- develop

Our origin/master , which is our Git's way of remembering master on origin , still remembers that commit X exists. That means that origin 's Git remembers X as being on their master .

That, in fact, is why we have to use git push --force origin master : to tell the Git at origin that it should discard its commit X . If we do this before anyone else—anyone who has access to that Git— also copies X into their Git repository, we're fine: no one saw X , so no one can be harmed by our removing X .

The problems start to pile up if someone else did grab a copy off of the other Git. Now there is some third Git repository that still has commit X , maybe in their master . Maybe they have built new commits atop (their copy of) X that they want to keep:

...--F--G--H--X--Y--Z   <-- master (HEAD)

We're now going to tell them: Oh, forget X , take it away from your repository too. That requires them to do their own git rebase --onto or similar, to copy their Y and Z to new commits that no longer lead back to X .

In short, by removing X from our Git and from origin 's Git, we put a burden on everyone else who shares these Git repositories: they, too, must all remove their X , and handle any consequences.

There are projects in which everyone agrees that this can happen—either at any time to any branch, or at any time to some specific subset(s) of branches. In these projects, resetting branches and force-pushing is fine. There are projects where there are no other users, or where you can force-push before anyone has a chance to pick up the mistake; in these cases, resetting and force-pushing is fine there too. The problems occur when you start making a lot of work for people who are not prepared to do it. In this case, making a new commit that simply un-does the work in X gives them a way to incorporate this new work in a way that they are prepared to accept.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM