0

I have the following situation: I have two remote branches: origin/master, origin/featureX

After a few changes in featureX something fundamental in master was added, that featureX shall base on.

so the tree looks like this:

m0 - m1 - m2(f) - m3      //<-- m2(f) is the fundamental change
  \- f1 - f2    - f3

now shortly said I want to change it to basically this:

m0 - m1 - m2(f) - m3
               \- f1 - f2 - f3

I looked around on stack overflow and found answers that gave the following possibilities:

  1. git rebase master

Is not an option, because as soon as feature1 rebases with master its history changes. And pushing that is bad, regardless of the server (in this case at least) forbidding non-fast-forward commits

  1. git merge featureX

This might be an option! But M3 would need to either not exist at the first place, or it would need to be included into the feature branch. Nevertheless the tree would then look like this:

m0 - m1 - m2(f) - m3
  \                 \
   - f1 - f2    - f3 - fm4

And I don't know if that would be the right thing, since it makes the history a bit confusing.

Would git merge in this case realy be the thing to do? Or is there a better option?

EDIT: I could belive the only option is to create a new (remote) branch beginning at m2(f) rebasing featureX branch into it and deleting featureX. But I would like to see a nicer solution if there is any

Apahdos
  • 97
  • 7
  • personally I am not against rewriting history on a feature branch; I would not recommending changing `master` history. Did you try `git push --force` on your feature branch after rewriting history? If it is really forbidden then merging would be your only option. – Chris Maes Aug 07 '19 at 10:07
  • FYI only yesterday I tried to explain the differences on rebase and master and which one to choose: https://stackoverflow.com/a/57378888/2082964 – Chris Maes Aug 07 '19 at 10:07
  • @ChrisMaes well, I have looked at the differences between merge and rebase the past couple of days and I think I know it. And I'm not realy comfortable with the git push --force idea, since people get angry, if they were working on something within the featureX branch, and I changed the history they are working on. I understand that git push --force is definitly the correct idea if I know, that noone has pulled before. Also the server still forbids it in this situation. I tried it ^^ – Apahdos Aug 07 '19 at 10:29
  • I could belive the only option is to create a new (remote) branch beginning at m2(f) rebasing featureX branch into it and deleting featureX. But I would like to see a nicer solution – Apahdos Aug 07 '19 at 10:45

1 Answers1

0

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:

  1. Find the merge base: that's commit *, in our case.
  2. Run two git diff --find-renames, 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.
  3. Attempt to combine these changes automatically.
  4. 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 name2s 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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Oh wow! Thank you for all that effort. This gave me a more clear thinking of how rebase worked! Even thought it did not quite give me the expected answer to my question it gave a very detailed tescription on when to use either of the two options I stated in the question. Especially for future readers this could get very helpful! Thanks – Apahdos Aug 08 '19 at 06:55