This can be done in Git, but it is more complicated. To understand why, and therefore, get to the point where we need to rethink the key difference between Mercurial and Git.
[Edit, a day or two later: I do not like to do this longer, but I think that I can now generalize the problem in two key points. It comes down to:
Mercurial allows you to use multiple heads. The mercurial concept of the tip of a branch is called the head inside the branch. When this situation occurs, Mercurial simply copes with it because it can and should.
DesignGit makes it impossible, by definition, to have multiple tip tips - Git refers to the concept of a branch tip as a tip on the same branch. This means that Git cannot have an equivalent, does not deal with this, and simply does not try. But we can do whatever we want using the built-in Git tools; he just gets messy.
The rest is a detailed explanation, as well as a way that gets pretty awkward, to do work with existing Git tools. What really needs to be done, what hg rebase does with just one command, is the best Git tool, but as far as I know, it does not exist. I wanted this for a while and started writing, but then the precedent itself disappeared, and I left it as a prototype that did just what I needed at that time.]
Affiliates vs. commits
In Git, the branch (name) is just a pointer to a single message:
...
The name branch1 is just a pointer that remembers the original commit D hash identifier. The name branch2 also a pointer that remembers the original commit F hash identifier.
All commits have their own personality, but commit A and B on both branches. Commits C and D reachable only through branch1 , commit E and F reachable only through branch2 , and commit A and B reachable with both names. In Git, this means that it means that the commit will be "on a branch."
In Mercurial, things are very different. A branch (name) is a very solid entity, and we can draw this graph as follows:
branch1: ...
Here, A is executed through D on branch1 . None of them are on branch2 . They can never be on branch2 . Fixes E and F on branch2 and they are forever stuck on branch2 and will never be on branch1 . It is true that we can merge branch2 back into branch1 by committing the commits E and F reachable, but in Mercurial reachability does not affect the grouping of commits into branches. Records are made on a branch and are permanently glued to this branch.
This, of course, means that trimming and re-grafting in Mercurial makes it obvious that commits are copied. New copies are located in another branch: they are clearly different from the originals. The meaning of the phrase “commit X is on branch Y” is constant and unchanged. The commit id depends on its branch.
In Git, however, the value "commit X on branch Y" is unstable. The end is on the branch only until the branch mark, which is temporary and movable, makes the commit available. Entries can be on many branches at the same time or even without any branch. The end has an identifier independent of the label label.
This allows Mercurial to have "multiple heads" inside the branch.
That Git calls the hint, Mercurial calls the head. Let me redraw your example, but Git -ified:
A
There are three tips here, so there are three branches named tip1 through tip3 . They identify fixations C , E and F
Mercury sticks are made into branches. This allows us to have a plug inside the branch. I cannot “make” the color, but suppose that the first line A--B--C is, say, yellow, and the remaining lines are green, indicating which commits are on which branches:
branch1: A--B--C \ branch2: D--E ... \ branch2: F
Here, branch2 contains both E and F commits, although they are different heads (which Git will call “hints”). This situation is not possible in Git, because for two different ends, two different branch names are required to point to them. You cannot draw one arrow to the right of it that points to E and F , which are in the green zone ( branch2 ).
Addendum (for editing): even the “tap-off”, Hg has more information than Git
Even if all the commits are on the same branch, Mercurial internals give it a direct ability that Git lacks. We can point to a commit (by hash identifier, serial number or Mercurial bookmark) and ask "all descendant threads that are in this branch". These are all the main commits, the branch of which is the current branch (and whose serial number is greater, although optimization), for which this commit is an ancestor. (Usually we consider fixing our own descendant and ancestor, and we are here too). This gives us (or hg) a quick way to find all the "interesting" chapters, and therefore all the commits for rebase.
Git commits has no equivalent: it is impossible to say that in the general case, commits are descendants of some kind of commit. Instead, we can only say which commits are the ancestors, following the internal identifiers of committing backwards (from obligations to their parents). The closest thing we can get to the Mercurial ability is to say, “starting at a specific tip of the branch and working in the opposite direction, see which hint branches have this obligation as their ancestor, use all these hint branches.” (Of course, --branches would be enough --branches , but this is something git rebase does not do, it is also quite slow.)
How to get what you want in Git
Since Git does not have multiple heads, and the Git branches are so ephemeral, we must start with our Git-specific drawing with three branches named tip1 through tip3 . Then we can change either tip2 or tip3 : the choice is arbitrary.
Like Mercurial, rebooting means copying. Let rebase tip2 get D' and E' . To begin with, I redrawn a bit to leave another room:
A
Now we run:
$ git checkout tip2 && git rebase tip1
First we get on branch tip2 , as git status would say, so our rebase will affect the tip2 pointer. He then instructs Git to find commits reachable from the current branch ( tip2 ) but not reachable from the given branch tip1 . These are commits D and E Then Git should copy these two commits, with copies placed after the --onto argument.
We did not give the argument --onto , but by default this argument gave, i.e. tip1 ; and tip1 indicates fixation C Thus, copies are placed after C The last step of rebase is to abandon the original chain of commits (although ORIG_HEAD and reflog for tip2 will remember them for a while) and make the current branch, i.e. tip2 , point to the final copied commit, i.e. E' :
A--B--C <-- tip1 \ \ \ D'-E' <-- tip2 \ D--E [ORIG_HEAD] \ F <-- tip3
We are halfway there. Now we come across the hard part: we also need to reinstall tip3 . We want our new copy of F' appear after commit D' . This means that we must find the identifier D' .
Finding this identifier is a bit more complicated. In this case, it is quite simple: it is the parent commit of the new E' and tip2 points to E' , so we just need to specify the parent tip2 for which any of these syntaxes works:
tip2^
(The difference between the syntax ^ and ~ is useful when you cross merge commands that have more than one parent. If we want to move more commits to a longer chain, we could repeat ^ many times, for example, foo^^^^^ , or use the syntax ~ : foo~5 For such cases, use the one that is easier for you to type.)
The naive attempt here that usually works is to simply run:
$ git checkout tip3 && git rebase tip2^
This reveals that commits are reachable from tip3 , but not from tip2^ . This, of course, is committing F itself and -hh oh-commit D Note that when we start with tip2^ and work backward, we go from D' to C to B So this reboot will copy both D and F , and not just copy commit F Copies will be sent after D' , which is the commit we identified by writing tip2^ .
It looks like a disaster: will we get a new copy of D'' ? And sometimes we will, and this is a (small) disaster. But when git rebase executes its copies, it first checks to see if the git rebase is copied in it, which, remember, is D , is a copy in the list of commits that it should skip.
The list of commits that he should skip are D' ( tip2^ ) and C ( tip2^^ or tip2~2 ). And, what you know, D' is a copy of D While git rebase can figure this out, it skips copying D in the end. Result:
A--B--C <-- tip1 \ \ \ D'-E' <-- tip2 \ \ DF' <-- tip3 \ F [ORIG_HEAD]
(What happened to E here? Answer: I don’t draw any reflog entries. Usually git log skips them, so I skip them too. I only include ORIG_HEAD , the special name that git rebase leaves behind. The old ORIG_HEAD pointed to E but the new rebase rewrote it, so now we only see commit F - and even then only if we use git log --all .)
Now there are cases when git rebase cannot understand that D copied to D' . In particular, this happens if the first rebase - the one that made tip2 pointer to E' - had a conflict that you had to manually resolve when copying D
In this case, you need the smart git rebase instead of the naive version. This is when you need the --onto argument:
$ git checkout tip3 && git rebase --onto tip2^ tip3^
This git rebase takes two parameters:
- A set of commits to exclude:
tip3^ , i.e. commit D and all earlier. - The place to start copying after:
tip2^ , i.e. commit D' .
This tells git rebase copy commit F , but not D or anything else before D , and put the copies after D' .
It would be nice if we could tell git rebase to do a few branch hints, but we cannot. It would be nice if we could Git figure out where D and D' automatically, and here we have a bit more luck, but it's still complicated. A few years ago, I started writing code along these lines, but I gave up the effort when I received too little benefit for too much pain. The cases that I really cared about were already handled by detecting the copy during the naive git rebase style.