You can—I won't say should, because for something like this I'd generally prefer a rebase approach myself—simply merge with the commit you care about.
I need more room in the graph-drawing, so let's redraw this:
m0 - m1 - m2(f) - m3 //<-- m2(f) is the fundamental change
\- f1 - f2 - f3
as:
A--B--C--D <-- origin/master
/
...--*
\
E--F--G <-- origin/featureX
where A
is what you called m0
, B
is what you called m1
, and so on. Commit *
is on both branches and represents the (single) hash ID that you would see if you ran:
git merge-base --all origin/master origin/featureX
Note that while we are using remote-tracking names origin/master
and origin/featureX
to identify specific commits D
and G
here, the branches consist of commits up through D
—including commit *
and anything earlier—and the commits through G
, including *
and earlier but not including the A-B-C-D
sequence. Git itself actually finds these commits by starting at the ends and working backwards: D
,then C
, then B
, then A
, then *
, and so on for one of the branches, and G
, then F
, then E
, then *
and so on for the other.
This is the key to the whole thing. A branch name identifies the last commit in the branch. Git calls this the tip of the branch. The branch itself consists of that commit, plus the tip commit's parent(s), plus the parents' parents, plus the parents of those, and so on, all the way back to the beginning of time for the Git repository. So if you had a branch name for commit D
—and you might; it might be your own master
—commits D
, then C
, then B
, then A
, and so on would be that branch as a whole, while commit D
would be the tip of that branch. You have a non-branch name—a remote-tracking name, origin/master
—for commit D
, so even though that's not a branch name, it will serve just as well.
But you don't even need a branch name! Pick out commit B
for a moment, by its hash ID. Call it the tip of this unnamed branch. Commit B
, then A
, then *
and so on, all form an anonymous branch. If you had a branch name identifying B
, that name would be a name for this branch. If you had two names for B
, that anonymous branch would be the same branch as the branches formed by either of those two names. Every commit is, or can be, the tip of a branch. You rarely need a branch name to use a commit as a branch. The main exception to this rule—the time when you do need a name—is when you're going to be using git fetch
or git push
to send both the name and the commit to some other Git.1
To make a new merge commit that combines C
(your m2(f)
) and G
(the tip of origin/featureX
), we'll want a new local branch name, but that's easy enough:
git checkout -b new-name --no-track origin/featureX
or more simply just:
git checkout featureX
if we don't have a featureX
yet and intend to add our new commit to origin's featureX
, so that after git push
, origin/featureX
identifies this new commit. I'll assume the latter, and draw this in:
A--B--C--D <-- origin/master
/
...--*
\
E--F--G <-- featureX (HEAD), origin/featureX
Now we simply run git merge
and give it the hash ID of commit C
, or anything else that specifies commit C
:
git merge <hash>
or:
git merge origin/master^
or:
git merge origin/master~1
C
is D
's parent, so any of the names master^
, master^1
, master~
, and master~1
all tell Git to find C
by starting at D
and stepping back to its first (and only) parent.
The git merge
command will now:
- Find the merge base: that's commit
*
, in our case.
- Run two
git diff --find-rename
s, to see what's different from *
to G
—what we changed on featureX
—and then again to see what's different from *
to C
: what they changed on the unnamed branch that at ends at commit master~1
.
- Attempt to combine these changes automatically.
- If all goes well in step 3, make a new merge commit. If not, stop in the middle of step 3, leaving you to clean up the mess.
Assuming all does go well, you wind up with this:
A--B--C--D <-- origin/master
/ \
...--* H <-- featureX (HEAD)
\ /
E--F--G <-- origin/featureX
and your merge is ready to git push
to origin
. If your git push
succeeds, your origin/featureX
will identify commit H
too, and featureX
in the Git repository over at origin
, wherever that may be, will identify commit H
.
Note that the hash IDs are universal across all Git repositories. The branch names are up to each repository, but by carefully using and adjusting each repository's branch names independently, you can make them match up.
This is why other people get annoyed if you rebase: rebase means copy some commits, then abandon the originals in favor of the new and improved copies. You had:
...--o--o--o <-- name1
\
A--B--C <-- name2
They—whoever "they" are—also have name2
identifying commit C
in their repository. Note that there are probably a half dozen people in the group "they", each of which has one repository with the name name2
. All those independent name2
s identify commit C
.
Now you run:
git checkout name2; git rebase name1
to throw away your original A-B-C
chain. The original chain remains in your own Git repository for some time, in case you change your mind, but you won't see it any more. You have now replaced it with this new-and-improved set of copied commits:
A'-B'-C' <-- name2
/
...--o--o--o <-- name1
\
A--B--C [abandoned]
You can then use git push --force
to make the Git over on origin forget its A-B-C
in favor of this new-and-improved A'-B'-C'
.
That updates the Git over at origin ... but now all those other people, the maybe-half-a-dozen people that "they" referred to, all have to update their Git repositories, to pick up the new A'-B'-C'
chain of commits and to make their name name2
identify commit C'
instead of C
.
If they've all agreed in advance that the name name2
gets moved around like this, they have no cause for complaint. If they haven't all agreed to that, then they're probably right to complain at you if you do this to them. Before moving someone else's branch names around in a non-fast-forward fashion, make sure they're OK with it. Rebases and other "history rewriting" options are non-fast-forward moves. Ordinary git merge
and git commit
operations tend to produce fast-forward moves.2
1Even with git push
, you only need a name for one end of the process. For instance, you can run:
git push origin <hash-id>:new-branch-name
to create branch name new-branch-name
on origin
, using a hash ID. The hash ID must be the ID of some existing commit in your own repository.
With git fetch
, you often need a name to pass to their Git:
git fetch origin <name>
which then becomes your origin/name
remote-tracking name. However, if their Git repository is configured to allow hash IDs, even here you can use git fetch origin hash-id:name
. Allowing hash IDs is not the default; whoever controls the Git at origin
must explicitly git config
the setting uploadpack.allowReachableSHA1InWant
or uploadpack.allowAnySHA1InWant
to enable this.
2The exception here—the reason to say tend to rather than always do—occurs when you and some other(s) are all working with one branch name, and each of you add a different new commit at the end. For instance, suppose we have:
...--o--o--H <-- name
in the Git repository at origin
. You make a clone, and make your own name name
to identify commit H
:
...--o--o--H <-- name, origin/name
Someone else makes a clone too, so they have exactly the same picture of their branches.
Now you make a new commit, which we can call I
. But they also make a new commit, which we can call J
. You and they both try to git push
your commits to origin
. Let's draw the commits that are now at origin
, leaving out the names:
I
/
...--o--o--H
\
J
The name name
on origin
can point to either I
or J
, but not both. Which commit should it use? Whichever one you pick, the other commit cannot be found in the repository, because to find a commit, you will start from a name and work backwards.
If the other guy beats you to the punch and pushes his commit J
successfully first, the picture is now:
...--o--o--H--J <-- name
in his repository and in the Git repository at origin
. (Well, his also has origin/name
to identify commit J
). You can now git fetch origin
to get this in your repository:
I <-- name
/
...--o--o--H--J <-- origin/name
You now have two options: copy I
to an I'
that comes after J
, then forget I
—this is a git rebase
–or merge I
with J
and make your name name
identify the merge commit. This rebase is "safe" as no one else has commit I
. Your attempt to git push origin name
failed: the Git at origin rejected it as a non-fast-forward. Since you are the only person who has commit I
, there cannot be any other names, in any other Git repositories, that have a name that identifies commit I
. You're free to replace I
with the new-and-improved I'
:
I [abandoned]
/
...--o--o--H--J <-- origin/name
\
I' <-- name
You can now git push origin name
to send them (origin) commit I'
, and ask them to set their name name
to identify I'
, and they probably will:
...--o--o--H--J--I' <-- name
Since hash IDs look random, and you probably don't even remember the hash ID of your original I
, you can be forgiven for forgetting that commit I
ever existed. The rewritten history looks like it was this way all along; no one but you need know that you did this rewrite, and you can even forget that you did it.