3

I'm wondering if there is a simple way to move commit from branch A to branch B.

I was working on "develop" branch, then created a new one, let's say "featureA" to start developing new feature. I made 2 commits, but first one should have gone to "develop" instead of "featureA" because it was just a small fix, but I forgot to change branch, so can I simply move commit 1 to branch "develop" and remove it from "featureA"?

Changes are still not pushed, just local.

I will appreciate any help.

2 Answers 2

4

In this particular case it is easy. To see why, draw (part of) the commit graph.

You started with something like this:

...--o--*   <-- develop (HEAD), origin/develop

You then created a new branch name, feature, pointing to the same tip-of-develop commit *. You attached your HEAD to the new branch, giving:

...--o--*   <-- develop, feature (HEAD), origin/develop

Now you made two new commits. Let's call them A and B rather than using their real hash IDs, which are big and ugly and forgettable. Commit A has, as its parent, commit *, and commit B has commit A as its parent, and the name feature now points to commit *:

...--o--*   <-- develop, origin/develop
         \
          A--B   <-- feature (HEAD)

Leaving feature alone, let's tell Git to move the name develop to point to commit A, the minor fix you want to have on your develop:

...--o--*   <-- origin/develop
         \
          A   <-- develop
           \
            B   <-- feature

I've removed the attached HEAD temporarily since there are several ways to adjust develop like this and HEAD may re-attach as we do them. This is the final picture you want: no commits have changed at all, but your local name develop now points to the first commit, while feature points to the second one.

So: how shall we do it?

One quite-safe method (can't lose any work) is to git checkout develop first, then use git merge --ff-only to fast-forward develop to the correct commit. That's commit A, which is one step back from feature, so we can do this:

git checkout develop
git merge --ff-only feature~1

This will leave you with develop pointing to commit A (and your HEAD attached to develop, so you have to git checkout feature to keep going).

Another way is to check out develop and use git reset --hard to move the label develop to any arbitrary commit (while also updating your index / staging-area and your work-tree). This is essentially the same as above, but it lets you move the name anywhere, even if that's not a sensible move, so the above is probably preferable.

Yet another way is to use git branch -f to force move the name to the location you want:

git branch -f develop feature~1

This leaves your HEAD attached to feature, so that you don't have to git checkout feature to keep working. However, like the git reset method, it's not very safe as it lets you move the name to any commit, even if that makes no sense.

Finally, there's a very sneaky way to update develop in a fast-forward fashion (a la git merge --ff-only) without checking out develop—in fact you need to stay on some other branch to do it:

git push . feature~1:develop

This has your Git call up your own Git, and propose to itself that it should move the name develop to point to the commit whose hash ID was obtained by parsing feature~1. Your Git will obey your Git if and only if this is a fast-forward, since there is no force flag here.

Use whichever of these you like; they all achieve the same result in the end, except for where your HEAD is attached.

6
  • Everything works as you wrote, thank you very much, but I'm a little bit confused about git merge --ff-only feature~1 command, can you elaborate more on this, or send some useful sources to learn more?
    – pablocity
    Commented Jun 28, 2018 at 20:22
  • @pablocity: git merge is a tricky little command because—like too many other Git commands, in my opinion—it can do one of several different things. One of these is a true merge, in which Git creates a new commit that has two (or more but let's not go there) parents. The other is what Git calls, variously, a fast-forward or, especially when done via git merge, a fast-forward merge (kind of a misnomer as there is no actual merging occurring!). Fast-forward is actually a property of label movement. Note that when we use git branch -f or git reset, we move some branch name ... 1/
    – torek
    Commented Jun 28, 2018 at 21:52
  • ... from wherever it pointed before, to any commit we choose. The motion of that label (in this case, a branch name) can be a fast-forward or a non-fast-forward, and what distinguishes these is whether the new commit has the original commit as an ancestor, i.e., whether, by starting at the new commit and working back through the commit graph, we can reach the previous commit. If we can reach the previous commit, this change was a fast-forward. 2/
    – torek
    Commented Jun 28, 2018 at 21:54
  • Both git push and git fetch favor fast-forward operations: they will happily adjust branch names, and for fetch, remote-tracking names, that result in fast-forwards. They object to non-fast-forward updates, requiring --force or equivalent. This guarantees that the change, whatever it is, preserves all the existing commits and merely adds some more (or in the degenerate case, changes nothing at all—the label already points to the new commit). Anyway, that's great for fetch and push, but what about merge? 3/
    – torek
    Commented Jun 28, 2018 at 21:57
  • When you do any git merge <commit-hash-or-name> operation, the merge code inspects the current commit (using HEAD to find its hash ID) and the target commit (whatever you named on the command line), and does one of these ancestor tests. If the current commit is an ancestor of the target commit, the merge operation is deemed unnecessary: a fast-forward will do. This is where --ff-only and --no-ff enter the picture: the former requires it and the latter forbids it, forcing Git to make a merge commit instead. 4/
    – torek
    Commented Jun 28, 2018 at 21:58
4

You can use git cherry-pick <commit> from the target branch to copy the commit there (<commit> can be the branch name if it's the last commit).

You can use git reset --hard HEAD^ to erase the last commit from the original branch.

5
  • 1
    Building on this, if the commits were already pushed to remote, git revert <commit> would be better in place of git reset --hard HEAD^ to "erase" the commit. Commented Jun 28, 2018 at 19:12
  • That's what I was looking for, but one thing, if commit which I wnat to remove isn't last one, can I use git reset --hard commitName to remove specific one?
    – pablocity
    Commented Jun 28, 2018 at 19:18
  • Question asked to move the commit; this will copy it. That can be corrected (either with more cherry-picking or a rebase), but it turns out to be overly complicated, considering that in this case the only thing "wrong" is the location of the branch refs, and the existing commits can be used as-is. See torek's answer. (And revert is nothing but cost in this case, since nothing has been pushed.) Commented Jun 28, 2018 at 19:35
  • @pablocity: No; that will reset to that commit, deleting everything after it. Use git rebase -i and delete the commit you want.
    – SLaks
    Commented Jun 28, 2018 at 19:42
  • This is all correct as well: if you do need to copy a commit (to change the graph's structure, instead of / in addition to changing which commit each name points-to), git cherry-pick will do it. Git books teach this stuff in what is in my opinion the wrong order: cherry-pick should come first, then rebase, because rebase is an automated series of cherry-picks. (And in all cases, the key to understanding any of it starts with the graph and how snapshots become change-sets.)
    – torek
    Commented Jun 28, 2018 at 22:05

Not the answer you're looking for? Browse other questions tagged or ask your own question.