Git equivalent of hg rebase -s source -d destination?

Is there git equivalent to hg rebase -s source -d newparent ?

That is, "trim" the branch to source and "translate" it to newparent . Or reparent source on newparent (merge, if necessary).

Or how to do it, for example:

 A - B - C \ D - E \ F 

For this:

 A - B - C \ D'- E' \ F' 

In this case, source is D , and newparent is B Running hg rebase -s D -d B gives the desired result. Is there a git equivalent?

I tried git rebase --onto BD but apparently didn't do anything except that it moved label shortcuts.

Edited for clarification: The goal is not to repeat the commit in the tree exactly the same as above. The above example. The goal is to allow me to re-commit a commit on top of any other commit, if there aren't any weird situations like trying to retry a merge commit or the like. I created a couple of scripts that recreate the tree above, one for hg :

 #!/bin/sh set -e rm -rf .hg hg init cat > .hg/hgrc <<'EOF' [ui] username = Rebase tester < no@email > [extensions] rebase = EOF echo A > file.txt hg add file.txt hg commit -m A_msg hg bookmark A_bm hg bookmark dummy # to stop A from tracking us echo B > file.txt hg commit -m B_msg hg bookmark B_bm hg bookmark graftpoint hg bookmark -f dummy echo C > file.txt hg commit -m C_msg hg bookmark C_bm hg checkout A_bm hg bookmark -f dummy echo D > file.txt hg commit -m D_msg hg bookmark D_bm hg bookmark prunepoint hg bookmark -f dummy echo E > file.txt hg commit -m E_msg hg bookmark E_bm hg checkout D_bm hg bookmark -f dummy echo F > file.txt hg commit -m F_msg hg bookmark F_bm hg bookmark -d dummy hg log -G -T '{desc} {bookmarks} {rev}:{node|short}' hg rebase -s D_bm -d B_bm -t internal:other hg log -G -T '{desc} {bookmarks} {rev}:{node|short}' 

and one for git:

 #!/bin/sh set -e rm -rf .git git init git config user.name 'Rebase tester' git config user.email ' no@email ' echo A > file.txt git add file.txt git commit -m A_msg git branch A_br echo B > file.txt git commit -a -m B_msg git branch B_br git branch graftpoint echo C > file.txt git commit -a -m C_msg git branch C_br git checkout A_br git checkout -b D_br echo D > file.txt git commit -a -m D_msg git branch prunepoint git checkout -b E_br echo E > file.txt git commit -a -m E_msg git checkout D_br git checkout -b F_br echo F > file.txt git commit -a -m F_msg git log --graph --all --format=format:'%s %d %h%n' #insert command(s) here git log --graph --all --format=format:'%s %d %h%n' 

The trees output by the first script are:

 @ F_msg F_bm 5:5ffe9c283d51 | | o E_msg E_bm 4:9f83c609d7b2 |/ o D_msg D_bm prunepoint 3:c3561e22f394 | | o C_msg C_bm 2:e7dd832a739b | | | o B_msg B_bm graftpoint 1:c3d6803dba3e |/ o A_msg A_bm 0:f52f4706cef0 

and

 @ F_msg F_bm 5:efe4fde4dcdf | | o E_msg E_bm 4:b2402cb25f70 |/ o D_msg D_bm prunepoint 3:5849595efdde | | o C_msg C_bm 2:e7dd832a739b |/ o B_msg B_bm graftpoint 1:c3d6803dba3e | o A_msg A_bm 0:f52f4706cef0 

exactly as expected. The output of the second script is

 * C_msg (master, C_br) de97063 | * B_msg (B_br) 6053c6b | | * E_msg (E_br) 13d4fac | | | | * F_msg (HEAD, F_br) b9ce3c4 | |/ | | | * D_msg (D_br) ed2ba19 |/ | * A_msg (A_br) 2cf9476 

(twice). The second should look something like this:

 * C_msg (master, C_br) de97063 | | * E_msg (E_br) 1398dc5 | | | | * F_msg (HEAD, F_br) 8ee34ad | |/ | | | * D_msg (D_br) ed873f7 |/ | * B_msg (B_br) 6053c6b | * A_msg (A_br) 2cf9476 

My problem is that hg rebase -s source -d destination works in any situation, but I have not found a way to do the same with git . I found a couple of third-party programs, but they don't seem to be considering this use case. One is git reparent and the other is git-reparent-branch . I also found a solution using grafts and a filter branch , but it did not seem to me that this handled conflicts correctly.

+5
source share
3 answers

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.

    Design
  • Git 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:

 ...--A--B--C--D <-- branch1 \ E--F <-- branch2 

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: ...--A--B--C--D \ branch2: E--F 

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--B--C <-- tip1 \ D--E <-- tip2 \ F <-- tip3 

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--B--C <-- tip1 \ \ \ D--E <-- tip2 \ F <-- tip3 

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^ # equivalent to tip2^1 tip2^1 # the first (and only) parent of the commit found via tip2 tip2~ # equivalent to tip2~1 tip2~1 # the commit found by moving one first-parent step back 

(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^ # I find ^ easier to type 

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.

+7
source

Judging by the torek answer, there is currently no automated way to do this. Therefore, the best way is probably to reproduce the tree manually. I say “tree” because I don’t know what will happen if merge commands are involved.

So, assuming fixation is trimming forms by an independent subtree, the following should work:

  • Find all downline commit tips for trimming (e.g. using gitk --all ).
  • Check the vaccination point.
  • Run git rebase --onto HEAD HEAD tip1 .
  • Repeat for each split point (branch points, i.e. commit, that have two or more children, possibly including the starting split point) in the subtree.
  • Correct links that are not hints, if any.

This is done in the case of an example:

 git checkout graftpoint set +e git rebase --onto HEAD HEAD E_br # Known conflict echo D > file.txt git add file.txt git rebase --continue set -e # Repeat for the branching point at E_br~1 git checkout E_br~1 git branch -f prunepoint # fix this reference while we're here git branch -f D_br # fix this reference while we're here git rebase --onto HEAD HEAD F_br 

This gives the desired schedule:

 * C_msg (master, C_br) 612ba5c | | * E_msg (E_br) 1b4fb2e | | | | * F_msg (HEAD, F_br) 78d5a1f | |/ | | | * D_msg (prunepoint, D_br) c4975c0 |/ | * B_msg (graftpoint, B_br) cc630ea | * A_msg (A_br) 01084ac 

(the hashes for A_br , B_br and C_br , of course, are the same as in the tree before the operation, different from those in the question because the tree was recreated).

0
source

Why is it so hard?

 git rebase --onto BAD git rebase --onto D D@ {1} E # or ... "--onto D{,@{1}} E" in bash git rebase --onto D D@ {1} F # or ... "--onto D{,@{1}} F" in bash 

must do the job (modulo merge conflict resolution).

I do this quite often that I wrote a script that does this automatically. It is a pity that Git does not provide its users with such a tool, but makes them write their own, but there is nothing wrong with the concepts in Git that prevents this; concepts in Git are simply different from concepts in Mercurial.

0
source

Source: https://habr.com/ru/post/1258989/


All Articles