GIT rebase requires to recommit changes

前端 未结 3 668
天涯浪人
天涯浪人 2021-01-14 11:30

Before anything else, I\'m just new to git branching. I wasn\'t aware that every feature branch should be branched out from master and only use the pre-requisit

相关标签:
3条回答
  • 2021-01-14 12:04

    Since you have pushed both feature branches already, you shouldn’t rebase at all. Rebasing already published branches is strongly discouraged since it breaks other developer’s repositories. The reason for that is that a rebase is just a complete rewrite of the commits. The commits you are rebasing are recreated, with changed contents, and—most importantly—with different hashes. This causes the new commits to be incompatible to the old ones, so whoever had the old ones ends up being in conflict with the new ones which replace them.

    The proper solution for this is to simply merge the changes. While this might not end up looking so pretty, it is a non-destructive action in which no existing commits are changed. All that happens is that commit are added which will result in no problems when pushing or pulling.

    That being said, you can rebase and still publish the changed branches. But to do so, you will need to force-push the branches and other developers pulling those changes will need to reset their branches to the new versions.


    To incorperate some of my comments below into the answer: It’s important to understand that in Git, branches are just pointers to commits. The whole history—without branches—is a large acyclic graph where commits just point to their parent commits. So to take your example from the question, this is the history, without any branch pointers:

    A -- B -- C -- D -- E
     \       /
      F --- G
             \
              H -- I -- J
    

    Each letter represents a commit and all commits one is connected to to the left are its parent. So for example F’s parent is A, and C is a merge commit with the parents B and G.

    Now, if we add branches to that visualization, then we just add pointers that point to some commits. It’s really nothing else (a branch is literally just a file that contains the hash of a commit):

                      master
                        ↓
    A -- B -- C -- D -- E
     \       /
      F --- G  ← feature-1
             \
              H -- I -- J
                        ↑
                    feature-2
    

    Now, imagine we make a commit to the feature-2 branch. We add that commit to the tree …

             \
              H -- I -- J -- K
                        ↑
                    feature-2
    

    … and then we move the branch pointer one forward:

             \
              H -- I -- J -- K
                             ↑
                         feature-2
    

    Now, to understand what happens during a push, we need to remember that remote branches are also just branches, and as such just another set of pointers. So it actually looks like this:

             \
              H -- I -- J ----------- K
                        ↑             ↑
               origin/feature-2    feature-2
    

    I think you can imagine what happens during a push now: We tell the remote repository to update its branch pointer so it points to K. But the server only has J, so we need to give the server everything to construct the tree accessible by K (so any other commits in between, and all the actual contents of those commits). But of course we don’t need to physically push J, or H, or even A (although those are all technically on the feature-2 branch since you can reach them); Git is smart enough to figure out which objects are actually missing (you can see Git calculating that when you start a push).

    So once we transferred the missing objects to the remote repository, we then tell the remote repository to update its feature-1 pointer, so it will also point at K. And if that succeeded, we also update our remote branch (origin/feature-2) to point to it too (just to get in sync).

    Now, the situation is really the same with merges. Imagine that we merged master into feature-2 (using git merge master while on feature-2):

                      master
                        ↓
    A -- B -- C -- D -- E -----
     \       /                 \
      F --- G  ← feature-1      \
             \                   \
              H -- I -- J -- K -- L
                                  ↑
                              feature-2
    

    Now, if we want to push feature-2, we again need to give the remote repository all the objects it doesn’t have. And since we are on a merge commit now, we need to check all parents: So if the server didn’t have K we would need to push K; But also, if it doesn’t have E, we would have to push E. And of course we need to follow those parents again to make sure that all objects exist on the remote. And once that’s done, we just tell the remote again to update the branch pointer.

    So to sum this up: A branch contains all commits that are somehow accessible by navigating the parents of its commits in the acyclic tree. But even if that means that branches are usually very “big” (in length of history), Git will only transfer those objects to the remote repository that it doesn’t have. So although a merge can add many more commits to a branch, those doesn’t necessarily have to be transferred if the remote already knows about them from another branch.


    And finally, some last words on rebasing: Above we did git merge master to merge the master branch into feature-2. If we did git rebase master instead, the full tree would look like this now:

                      master               feature-2
                        ↓                      ↓
    A -- B -- C -- D -- E -- H' -- I' -- J' -- K'
     \       /
      F --- G  ← feature-1
             \
              H -- I -- J -- K
                             ↑
                      origin/feature-2
    

    As you can see, there are new commits H', I', J' and K'. These are the rewritten commits so that they start at E (where master pointed to at the time of the rebase) instead of G. Since we rebased, there is no merge commit L. And as made clear above, the original commits still exist. It’s just that there is no pointer left that points to them; so they are “lost” and will eventually be garbage collected.

    So what is the problem when pushing now? The remote branch still points to the original K, but we want it to point to K' now. So we start giving the remote repository all the objects it needs, just like before. But when we tell it to set update the branch pointer, it will refuse to do that. The reason for that is that by setting the pointer to K' it would have to “go back in the history” and ignore the existence of commits H to K. It doesn’t know that we have rebased those and there is also no link between the rewritten ones and the original ones. So just to prevent accidental data loss, the remote will refuse to update the branch pointer.

    Now, you can force push the branch. This will tell the remote repository to update the branch pointer even though doing so would throw those original commits away. So you do that, and the situation will look like this:

                                       origin/feature-2
                      master               feature-2
                        ↓                      ↓
    A -- B -- C -- D -- E -- H' -- I' -- J' -- K'
     \       /
      F --- G  ← feature-1
    

    So far, everything is well: You decided to rebase the branch, and you told the remote repository to accept that without questioning it. But now imagine I wanted to pull that; and my branch is still pointing to I. So running pull does the same as a push in reverse: The remote gives me all the objects necessary to complete the history and then it tells me where to set the branch pointer. And at that point, my local Git refuses to do so for the same reason the remote repository did that before.

    With the push before, we had the knowledge that we wanted to replace the original commits; but with a pull we don’t have that, so we are now required to investigate, or ask around, whether we should just replace our local branch, or if there’s actually some fault on the remote. And the situation gets even worse if we did some work on our own locally which we would want to merge now.

    And these problems happen to everyone that fetched those original commits once. In general, you want to avoid this mess completely, so the rule is to never rebase something that you have already published. You can rebase as long as nobody else ever got those original commits, but as soon as that’s no longer the case, it will be a mess for everyone involved. So a merge is definitely preferred.

    0 讨论(0)
  • 2021-01-14 12:18

    As far as I understand, the rules of your codebase (team) demand you to rebase your feature branch against the master. You could do that by saying git rebase --onto master A2 feature-2 which would mean "take commits of the feature-2 starting with A2 exclusive and place them on top of the master". You could then push your changes directly to master and drop or keep the feature-2 branch intact, again depending on the workflow conventions.

    If on the other hand, the rebase is not demanded - you could well do a simple merge of the feature-2 into master, pushing the change to the master, as @poke recommends.

    0 讨论(0)
  • 2021-01-14 12:21

    Is there a way to rebase feature-2 to master without the need to push again the commits of feature-2

    Not really, considering a rebase will replay the commits of feature-2 on top of master, given you this:

    M1 -- M2 -- M3 -- M4 -- M5   [master]
      \        /              \
       A1 --- A2 [feature-1]   A1' --- A2'
                                         \
                                          B1' -- B2' -- B3'   [feature-2]
    

    As the ' sign indicates, all the commits have changed (their SHA1 is different).

    Since A1 and A2 were already merged into master, a better rebase would be

    git rebase --onto master A2 feature-2
    

    That would get:

    M1 -- M2 -- M3 -- M4 -- M5   [master]
      \        /              \
       A1 --- A2 [feature-1]   B1' -- B2' -- B3'   [feature-2]
    

    You should git push --force that newly revised feature-2 branch in order to update your pull request.

    A forced push should update a pull request in Bitbucket since it supports such a push since Q4 2012 (issue 4913).
    A forced push won't have any other adverse effects, considering you are pushing a branch to your fork where, presumably, you are the only one to push updates.
    Since the pull request will automatically updates itself with the new history of feature-2, that won't compromise said pull-request.

    0 讨论(0)
提交回复
热议问题