You can get this:
C--D <-- new-branch
/
A--B <-- master
but to do that, you must force all Git repositories that have a branch named master
pointing to existing commit D
to retract their master
to point to existing commit B
. That can be difficult, since Git "likes" to add commits, but not to "remove" them.
To get this locally, assuming you have:
A--B--C--D <-- master (HEAD)
run:
git branch new-branch master
git reset --hard HEAD~2
You now have:
C--D <-- new-branch
/
A--B <-- master (HEAD)
locally. Unfortunately, the other Git repository over at origin
still has its master
pointing to commit D
. Any clones made from that other Git repository also have their master
pointing to commit D
. You must get all of those repositories to do the same kind of retraction. If there is only the one repository, and you have permission to use git push --force
to retract it, you can now:
git push origin new-branch # to create new-branch on origin
git push -f origin master # to force them to retract their master
(You may want to add -u
to the first git push
.)
To get the safer thing you're looking for—adding a new commit to master
that resets it back to the way it was at commit B
—you can use:
git branch temp master
which sets things up to read:
A--B--C--D <-- master (HEAD), temp
Then you can run git revert -n HEAD~2..HEAD; git commit
, or git read-tree -u HEAD~2; git commit
to make a new commit E
whose snapshot matches that of B
:
A--B--C--D <-- temp
\
E <-- master (HEAD)
Existing commits C-D
are stuck where they are. If you want new-branch
to be a descendant of master
, you must now create it. Note that, if you are OK with new-branch
pointing to existing commit D
, you can just use the name new-branch
instead of temp
and you will have what you wanted. But if you need a new commit—let's call it F
—that comes after E
that matches the contents of commit D
, you can create new-branch now:
git checkout -b new-branch master
giving:
A--B--C--D <-- temp
\
E <-- master, new-branch (HEAD)
Now you can:
- revert the reversion commit
E
, or
git read-tree
commit D
and commit, or
git cherry-pick
commits C
and D
.
To do the first, run git revert master
or git revert HEAD
(either suffices) and you have:
A--B--C--D <-- temp
\
E <-- master
\
F <-- new-branch (HEAD)
To do the second, run git read-tree -u temp; git commit
to make new commit F
(you will need to enter a new commit message).
To do the third, run git cherry-pick temp~2..temp
; this will produce:
A--B--C--D <-- temp
\
E <-- master
\
C'-D' <-- new-branch (HEAD)
where the names C'
and D'
indicate that these are cherry-picked copies of C
and D
respectively. (When we reverted to make E
we combined both undoing steps, using git revert -n
, so E
is not exactly an undo of either D
or C
, but rather an undo of both at once.)
It's now safe to remove the name temp
as we do not need it to find C
and D
conveniently any more. (We did not need it all along, as we could have found them anyway, but it was convenient to have a name for commit D
so that commits C
and B
were temp~2
and temp~1
respectively.)
It's now safe (and does not require a git push --force
) to git push origin master new-branch
.
Alternatively, you could, instead of creating temp
, make the name new-branch
point to commit D
all along. That is, starting with:
A--B--C--D <-- master (HEAD)
you could do:
git branch new-branch master
giving:
A--B--C--D <-- master (HEAD), new-branch
Then you can "undo" the C-D
commits on master
as before, e.g., producing:
A--B--C--D <-- new-branch
\
E <-- master (HEAD)
As the next step, you could run:
git checkout new-branch; git merge -s ours master
to make Git create a new merge commit F
whose snapshot matches that of existing commit D
:
A--B--C--D---F <-- new-branch
\ /
E <-- master (HEAD)
Commit F
is now ahead of master
as before, but when reading backwards in a straight line, across first-parent links, it never visits commit E
: Git walks back from F
to D
to C
and so on. Again, this is provided you use --first-parent
when having git log
or git rev-list
traverse back from F
. By default, a traversal back from F
visits both commits E
and D
(and since the default is to go by committer-date order, you'll see E
, which we made later than D
, before seeing D
).