Is it possible to do [what I want] with one single git commit?
I assume here you mean one Git command and not one Git commit. The answer is: no, it's not possible to do that.
How can I move a1
out of foo
into bar
, so that bar
's history is root -> a1 -> a2
and a1
is not in foo
?
See matt's answer for a way to do this with git rebase --onto
.
It may, however, help to draw this the way Git sees it. It's not root -> a1 -> a2
. It's root <-a1 <-a2
. The name foo
itself can be moved, but the three existing commits here are carved in stone: no parts of them can be changed. That's OK since, as you said, none of those commits have been sent anywhere else.
No matter what you do, you must copy some commit(s) to new-and-improved commits, with different hash IDs. You can leave the existing carved-in-stone commits alone provided you're changing nothing about them, including the backwards-pointing arrows that are part of the commits.
The root commit (really: 6ebca20
) has no backwards-pointing arrow. That's what makes it a root commit. As long as you don't dislike anything about this commit, you can leave it alone—which is good since root commits are a pain to copy. Both git cherry-pick
and git rebase
can do that, but it's all a little weird and special-case-y, since the cherry-pick or rebase operation works by comparing a commit's snapshot to its parent's snapshot. The fact that there is no parent of a root commit is what makes it a little weird.1
Commit a1
(really: 4656a98
) points backwards to the root commit. That too seems OK.
Commit b1
, however—which is really 9b29ae3
—points backwards to commit 4656a98
. That's set in stone. It cannot be changed. You can make a new and different commit, that you also call b1
, that instead points backwards to the root commit, and that's what you want to do.
Having done this with b1
, you now need to copy commit b2
to a new-and-improved commit, because the existing b2
(d332e4d
) points back to 9b29ae3
. You need a copy that makes the same changes—something cherry-pick will do for you—but that has the new b1
, whatever its hash ID becomes, as its parent.
Once you've copied b1
and b2
, you can point the name foo
at the copy of b2
. Branch names, in Git, simply point to some actual, existing commit. You can change which commit they point to, at any time; whatever commit they point to is the last commit in the branch. Earlier commits are also in the branch by virtue of working backwards through the parent linkage in the last commit, then working backwards another step to the parent's parent (or parents' parents or whatever).
Since you must copy b1
to a new-and-improved version, this forces you to copy b2
as well. The git rebase
command runs git cherry-pick
repeatedly to achieve said copying, then—once all the copying is done—moves the branch name to point to the last copied commit. So one git rebase
, with the right options (--onto
included), will do the trick for the foo
branch.
Separately, you must copy the existing a2
to a new-and-improved version. Having made the copy, you must then move the name bar
to point to the copied a2
. One git rebase
command suffices to do all of these operations as well.
Because git rebase
can run git checkout
for you, the minimum number of Git commands needed is, at this point, two. That leads to the set of commands that matt used: the two extra ones were for convenience.
1The cherry-pick code itself handles this by using a faked-up "has no files" parent, temporarily, so that as far as the cherry-pick operation is concerned, the "change" made by the commit is add all the files. The rebase code requires the --root
option, at least in older versions of Git; I'm not sure what has happened here since the sequencer has now learned to do the interactive stuff that used to be in shell scripts.