When do you use Git rebase instead of Git merge?

后端 未结 17 2385
独厮守ぢ
独厮守ぢ 2020-11-21 11:25

When is it recommended to use Git rebase vs. Git merge?

Do I still need to merge after a successful rebase?

相关标签:
17条回答
  • 2020-11-21 12:17

    Before merge/rebase:

    A <- B <- C    [master]
    ^
     \
      D <- E       [branch]
    

    After git merge master:

    A <- B <- C
    ^         ^
     \         \
      D <- E <- F
    

    After git rebase master:

    A <- B <- C <- D' <- E'
    

    (A, B, C, D, E and F are commits)

    This example and much more well illustrated information about Git can be found in Git The Basics Tutorial.

    0 讨论(0)
  • 2020-11-21 12:19

    To complement my own answer mentioned by TSamper,

    • a rebase is quite often a good idea to do before a merge, because the idea is that you integrate in your branch Y the work of the branch B upon which you will merge.
      But again, before merging, you resolve any conflict in your branch (i.e.: "rebase", as in "replay my work in my branch starting from a recent point from the branch B).
      If done correctly, the subsequent merge from your branch to branch B can be fast-forward.

    • a merge directly impacts the destination branch B, which means the merges better be trivial, otherwise that branch B can be long to get back to a stable state (time for you solve all the conflicts)


    the point of merging after a rebase?

    In the case that I describe, I rebase B onto my branch, just to have the opportunity to replay my work from a more recent point from B, but while staying into my branch.
    In this case, a merge is still needed to bring my "replayed" work onto B.

    The other scenario (described in Git Ready for instance), is to bring your work directly in B through a rebase (which does conserve all your nice commits, or even give you the opportunity to re-order them through an interactive rebase).
    In that case (where you rebase while being in the B branch), you are right: no further merge is needed:

    A Git tree at default when we have not merged nor rebased

    rebase1

    we get by rebasing:

    rebase3

    That second scenario is all about: how do I get new-feature back into master.

    My point, by describing the first rebase scenario, is to remind everyone that a rebase can also be used as a preliminary step to that (that being "get new-feature back into master").
    You can use rebase to first bring master "in" the new-feature branch: the rebase will replay new-feature commits from the HEAD master, but still in the new-feature branch, effectively moving your branch starting point from an old master commit to HEAD-master.
    That allows you to resolve any conflicts in your branch (meaning, in isolation, while allowing master to continue to evolve in parallel if your conflict resolution stage takes too long).
    Then you can switch to master and merge new-feature (or rebase new-feature onto master if you want to preserve commits done in your new-feature branch).

    So:

    • "rebase vs. merge" can be viewed as two ways to import a work on, say, master.
    • But "rebase then merge" can be a valid workflow to first resolve conflict in isolation, then bring back your work.
    0 讨论(0)
  • 2020-11-21 12:19

    A lot of answers here say that merging turns all your commits into one, and therefore suggest to use rebase to preserve your commits. This is incorrect. And a bad idea if you have pushed your commits already.

    Merge does not obliterate your commits. Merge preserves history! (just look at gitk) Rebase rewrites history, which is a Bad Thing after you've pushed it.

    Use merge -- not rebase whenever you've already pushed.

    Here is Linus' (author of Git) take on it (now hosted on my own blog, as recovered by the Wayback Machine). It's a really good read.

    Or you can read my own version of the same idea below.

    Rebasing a branch on master:

    • provides an incorrect idea of how commits were created
    • pollutes master with a bunch of intermediate commits that may not have been well tested
    • could actually introduce build breaks on these intermediate commits because of changes that were made to master between when the original topic branch was created and when it was rebased.
    • makes finding good places in master to checkout difficult.
    • Causes the timestamps on commits to not align with their chronological order in the tree. So you would see that commit A precedes commit B in master, but commit B was authored first. (What?!)
    • Produces more conflicts, because individual commits in the topic branch can each involve merge conflicts which must be individually resolved (further lying in history about what happened in each commit).
    • is a rewrite of history. If the branch being rebased has been pushed anywhere (shared with anyone other than yourself) then you've screwed up everyone else who has that branch since you've rewritten history.

    In contrast, merging a topic branch into master:

    • preserves history of where topic branches were created, including any merges from master to the topic branch to help keep it current. You really get an accurate idea of what code the developer was working with when they were building.
    • master is a branch made up mostly of merges, and each of those merge commits are typically 'good points' in history that are safe to check out, because that's where the topic branch was ready to be integrated.
    • all the individual commits of the topic branch are preserved, including the fact that they were in a topic branch, so isolating those changes is natural and you can drill in where required.
    • merge conflicts only have to be resolved once (at the point of the merge), so intermediate commit changes made in the topic branch don't have to be resolved independently.
    • can be done multiple times smoothly. If you integrate your topic branch to master periodically, folks can keep building on the topic branch, and it can keep being merged independently.
    0 讨论(0)
  • 2020-11-21 12:21

    I just created a FAQ for my team in my own words which answers this question. Let me share:

    What is a merge?

    A commit, that combines all changes of a different branch into the current.

    What is a rebase?

    Re-comitting all commits of the current branch onto a different base commit.

    What are the main differences between merge and rebase?

    1. merge executes only one new commit. rebase typically executes multiple (number of commits in current branch).
    2. merge produces a new generated commit (the so called merge-commit). rebase only moves existing commits.

    In which situations should we use a merge?

    Use merge whenever you want to add changes of a branched out branch back into the base branch.

    Typically, you do this by clicking the "Merge" button on Pull/Merge Requests, e.g. on GitHub.

    In which situations should we use a rebase?

    Use rebase whenever you want to add changes of a base branch back to a branched out branch.

    Typically, you do this in feature branches whenever there's a change in the main branch.

    Why not use merge to merge changes from the base branch into a feature branch?

    1. The git history will include many unnecessary merge commits. If multiple merges were needed in a feature branch, then the feature branch might even hold more merge commits than actual commits!

    2. This creates a loop which destroys the mental model that Git was designed by which causes troubles in any visualization of the Git history.

      Imagine there's a river (e.g. the "Nile"). Water is flowing in one direction (direction of time in Git history). Now and then, imagine there's a branch to that river and suppose most of those branches merge back into the river. That's what the flow of a river might look like naturally. It makes sense.

      But then imagine there's a small branch of that river. Then, for some reason, the river merges into the branch and the branch continues from there. The river has now technically disappeared, it's now in the branch. But then, somehow magically, that branch is merged back into the river. Which river you ask? I don't know. The river should actually be in the branch now, but somehow it still continues to exist and I can merge the branch back into the river. So, the river is in the river. Kind of doesn't make sense.

      This is exactly what happens when you merge the base branch into a feature branch and then when the feature branch is done, you merge that back into the base branch again. The mental model is broken. And because of that, you end up with a branch visualization that's not very helpful.

    Example Git History when using merge:

    Note the many commits starting with Merge branch 'develop' into .... They don't even exist if you rebase (there, you will only have pull request merge commits). Also many visual branch merge loops (develop into feature into develop).

    Example Git History when using rebase:

    Much cleaner Git history with much less merge commits and no cluttered visual branch merge loops whatsoever.

    Are there any downsides / pitfalls with rebase?

    Yes:

    1. Because a rebase moves commits (technically re-executes them), the commit date of all moved commits will be the time of the rebase and the git history loses the initial commit time. So, if the exact date of a commit is needed for some reason, then merge is the better option. But typically, a clean git history is much more useful than exact commit dates.
    2. If the rebased branch has multiple commits that change the same line and that line was also changed in the base branch, you might need to solve merge conflicts for that same line multiple times, which you never need to do when merging. So, on average, there's more merge conflicts to solve.

    Tips to reduce merge conflicts when using rebase:

    1. Rebase often. I typically recommend doing it at least once a day.
    2. Try to squash changes on the same line into one commit as much as possible.
    0 讨论(0)
  • 2020-11-21 12:22

    This answer is widely oriented around Git Flow. The tables have been generated with the nice ASCII Table Generator, and the history trees with this wonderful command (aliased as git lg):

    git log --graph --abbrev-commit --decorate --date=format:'%Y-%m-%d %H:%M:%S' --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%ad%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n''          %C(white)%s%C(reset) %C(dim white)- %an%C(reset)'
    

    Tables are in reverse chronological order to be more consistent with the history trees. See also the difference between git merge and git merge --no-ff first (you usually want to use git merge --no-ff as it makes your history look closer to the reality):

    git merge

    Commands:

    Time          Branch "develop"             Branch "features/foo"
    ------- ------------------------------ -------------------------------
    15:04   git merge features/foo
    15:03                                  git commit -m "Third commit"
    15:02                                  git commit -m "Second commit"
    15:01   git checkout -b features/foo
    15:00   git commit -m "First commit"
    

    Result:

    * 142a74a - YYYY-MM-DD 15:03:00 (XX minutes ago) (HEAD -> develop, features/foo)
    |           Third commit - Christophe
    * 00d848c - YYYY-MM-DD 15:02:00 (XX minutes ago)
    |           Second commit - Christophe
    * 298e9c5 - YYYY-MM-DD 15:00:00 (XX minutes ago)
                First commit - Christophe
    

    git merge --no-ff

    Commands:

    Time           Branch "develop"              Branch "features/foo"
    ------- -------------------------------- -------------------------------
    15:04   git merge --no-ff features/foo
    15:03                                    git commit -m "Third commit"
    15:02                                    git commit -m "Second commit"
    15:01   git checkout -b features/foo
    15:00   git commit -m "First commit"
    

    Result:

    *   1140d8c - YYYY-MM-DD 15:04:00 (XX minutes ago) (HEAD -> develop)
    |\            Merge branch 'features/foo' - Christophe
    | * 69f4a7a - YYYY-MM-DD 15:03:00 (XX minutes ago) (features/foo)
    | |           Third commit - Christophe
    | * 2973183 - YYYY-MM-DD 15:02:00 (XX minutes ago)
    |/            Second commit - Christophe
    * c173472 - YYYY-MM-DD 15:00:00 (XX minutes ago)
                First commit - Christophe
    

    git merge vs git rebase

    First point: always merge features into develop, never rebase develop from features. This is a consequence of the Golden Rule of Rebasing:

    The golden rule of git rebase is to never use it on public branches.

    In other words:

    Never rebase anything you've pushed somewhere.

    I would personally add: unless it's a feature branch AND you and your team are aware of the consequences.

    So the question of git merge vs git rebase applies almost only to the feature branches (in the following examples, --no-ff has always been used when merging). Note that since I'm not sure there's one better solution (a debate exists), I'll only provide how both commands behave. In my case, I prefer using git rebase as it produces a nicer history tree :)

    Between feature branches

    git merge

    Commands:

    Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"
    ------- -------------------------------- ------------------------------- --------------------------------
    15:10   git merge --no-ff features/bar
    15:09   git merge --no-ff features/foo
    15:08                                                                    git commit -m "Sixth commit"
    15:07                                                                    git merge --no-ff features/foo
    15:06                                                                    git commit -m "Fifth commit"
    15:05                                                                    git commit -m "Fourth commit"
    15:04                                    git commit -m "Third commit"
    15:03                                    git commit -m "Second commit"
    15:02   git checkout -b features/bar
    15:01   git checkout -b features/foo
    15:00   git commit -m "First commit"
    

    Result:

    *   c0a3b89 - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)
    |\            Merge branch 'features/bar' - Christophe
    | * 37e933e - YYYY-MM-DD 15:08:00 (XX minutes ago) (features/bar)
    | |           Sixth commit - Christophe
    | *   eb5e657 - YYYY-MM-DD 15:07:00 (XX minutes ago)
    | |\            Merge branch 'features/foo' into features/bar - Christophe
    | * | 2e4086f - YYYY-MM-DD 15:06:00 (XX minutes ago)
    | | |           Fifth commit - Christophe
    | * | 31e3a60 - YYYY-MM-DD 15:05:00 (XX minutes ago)
    | | |           Fourth commit - Christophe
    * | |   98b439f - YYYY-MM-DD 15:09:00 (XX minutes ago)
    |\ \ \            Merge branch 'features/foo' - Christophe
    | |/ /
    |/| /
    | |/
    | * 6579c9c - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)
    | |           Third commit - Christophe
    | * 3f41d96 - YYYY-MM-DD 15:03:00 (XX minutes ago)
    |/            Second commit - Christophe
    * 14edc68 - YYYY-MM-DD 15:00:00 (XX minutes ago)
                First commit - Christophe
    

    git rebase

    Commands:

    Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"
    ------- -------------------------------- ------------------------------- -------------------------------
    15:10   git merge --no-ff features/bar
    15:09   git merge --no-ff features/foo
    15:08                                                                    git commit -m "Sixth commit"
    15:07                                                                    git rebase features/foo
    15:06                                                                    git commit -m "Fifth commit"
    15:05                                                                    git commit -m "Fourth commit"
    15:04                                    git commit -m "Third commit"
    15:03                                    git commit -m "Second commit"
    15:02   git checkout -b features/bar
    15:01   git checkout -b features/foo
    15:00   git commit -m "First commit"
    

    Result:

    *   7a99663 - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)
    |\            Merge branch 'features/bar' - Christophe
    | * 708347a - YYYY-MM-DD 15:08:00 (XX minutes ago) (features/bar)
    | |           Sixth commit - Christophe
    | * 949ae73 - YYYY-MM-DD 15:06:00 (XX minutes ago)
    | |           Fifth commit - Christophe
    | * 108b4c7 - YYYY-MM-DD 15:05:00 (XX minutes ago)
    | |           Fourth commit - Christophe
    * |   189de99 - YYYY-MM-DD 15:09:00 (XX minutes ago)
    |\ \            Merge branch 'features/foo' - Christophe
    | |/
    | * 26835a0 - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)
    | |           Third commit - Christophe
    | * a61dd08 - YYYY-MM-DD 15:03:00 (XX minutes ago)
    |/            Second commit - Christophe
    * ae6f5fc - YYYY-MM-DD 15:00:00 (XX minutes ago)
                First commit - Christophe
    

    From develop to a feature branch

    git merge

    Commands:

    Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"
    ------- -------------------------------- ------------------------------- -------------------------------
    15:10   git merge --no-ff features/bar
    15:09                                                                    git commit -m "Sixth commit"
    15:08                                                                    git merge --no-ff develop
    15:07   git merge --no-ff features/foo
    15:06                                                                    git commit -m "Fifth commit"
    15:05                                                                    git commit -m "Fourth commit"
    15:04                                    git commit -m "Third commit"
    15:03                                    git commit -m "Second commit"
    15:02   git checkout -b features/bar
    15:01   git checkout -b features/foo
    15:00   git commit -m "First commit"
    

    Result:

    *   9e6311a - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)
    |\            Merge branch 'features/bar' - Christophe
    | * 3ce9128 - YYYY-MM-DD 15:09:00 (XX minutes ago) (features/bar)
    | |           Sixth commit - Christophe
    | *   d0cd244 - YYYY-MM-DD 15:08:00 (XX minutes ago)
    | |\            Merge branch 'develop' into features/bar - Christophe
    | |/
    |/|
    * |   5bd5f70 - YYYY-MM-DD 15:07:00 (XX minutes ago)
    |\ \            Merge branch 'features/foo' - Christophe
    | * | 4ef3853 - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)
    | | |           Third commit - Christophe
    | * | 3227253 - YYYY-MM-DD 15:03:00 (XX minutes ago)
    |/ /            Second commit - Christophe
    | * b5543a2 - YYYY-MM-DD 15:06:00 (XX minutes ago)
    | |           Fifth commit - Christophe
    | * 5e84b79 - YYYY-MM-DD 15:05:00 (XX minutes ago)
    |/            Fourth commit - Christophe
    * 2da6d8d - YYYY-MM-DD 15:00:00 (XX minutes ago)
                First commit - Christophe
    

    git rebase

    Commands:

    Time           Branch "develop"              Branch "features/foo"           Branch "features/bar"
    ------- -------------------------------- ------------------------------- -------------------------------
    15:10   git merge --no-ff features/bar
    15:09                                                                    git commit -m "Sixth commit"
    15:08                                                                    git rebase develop
    15:07   git merge --no-ff features/foo
    15:06                                                                    git commit -m "Fifth commit"
    15:05                                                                    git commit -m "Fourth commit"
    15:04                                    git commit -m "Third commit"
    15:03                                    git commit -m "Second commit"
    15:02   git checkout -b features/bar
    15:01   git checkout -b features/foo
    15:00   git commit -m "First commit"
    

    Result:

    *   b0f6752 - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)
    |\            Merge branch 'features/bar' - Christophe
    | * 621ad5b - YYYY-MM-DD 15:09:00 (XX minutes ago) (features/bar)
    | |           Sixth commit - Christophe
    | * 9cb1a16 - YYYY-MM-DD 15:06:00 (XX minutes ago)
    | |           Fifth commit - Christophe
    | * b8ffffd19 - YYYY-MM-DD 15:05:00 (XX minutes ago)
    |/            Fourth commit - Christophe
    *   856433e - YYYY-MM-DD 15:07:00 (XX minutes ago)
    |\            Merge branch 'features/foo' - Christophe
    | * 694ac81 - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)
    | |           Third commit - Christophe
    | * 5fd94d3 - YYYY-MM-DD 15:03:00 (XX minutes ago)
    |/            Second commit - Christophe
    * d01d589 - YYYY-MM-DD 15:00:00 (XX minutes ago)
                First commit - Christophe
    

    Side notes

    git cherry-pick

    When you just need one specific commit, git cherry-pick is a nice solution (the -x option appends a line that says "(cherry picked from commit...)" to the original commit message body, so it's usually a good idea to use it - git log <commit_sha1> to see it):

    Commands:

    Time           Branch "develop"              Branch "features/foo"                Branch "features/bar"
    ------- -------------------------------- ------------------------------- -----------------------------------------
    15:10   git merge --no-ff features/bar
    15:09   git merge --no-ff features/foo
    15:08                                                                    git commit -m "Sixth commit"
    15:07                                                                    git cherry-pick -x <second_commit_sha1>
    15:06                                                                    git commit -m "Fifth commit"
    15:05                                                                    git commit -m "Fourth commit"
    15:04                                    git commit -m "Third commit"
    15:03                                    git commit -m "Second commit"
    15:02   git checkout -b features/bar
    15:01   git checkout -b features/foo
    15:00   git commit -m "First commit"
    

    Result:

    *   50839cd - YYYY-MM-DD 15:10:00 (XX minutes ago) (HEAD -> develop)
    |\            Merge branch 'features/bar' - Christophe
    | * 0cda99f - YYYY-MM-DD 15:08:00 (XX minutes ago) (features/bar)
    | |           Sixth commit - Christophe
    | * f7d6c47 - YYYY-MM-DD 15:03:00 (XX minutes ago)
    | |           Second commit - Christophe
    | * dd7d05a - YYYY-MM-DD 15:06:00 (XX minutes ago)
    | |           Fifth commit - Christophe
    | * d0d759b - YYYY-MM-DD 15:05:00 (XX minutes ago)
    | |           Fourth commit - Christophe
    * |   1a397c5 - YYYY-MM-DD 15:09:00 (XX minutes ago)
    |\ \            Merge branch 'features/foo' - Christophe
    | |/
    |/|
    | * 0600a72 - YYYY-MM-DD 15:04:00 (XX minutes ago) (features/foo)
    | |           Third commit - Christophe
    | * f4c127a - YYYY-MM-DD 15:03:00 (XX minutes ago)
    |/            Second commit - Christophe
    * 0cf894c - YYYY-MM-DD 15:00:00 (XX minutes ago)
                First commit - Christophe
    

    git pull --rebase

    I am not sure I can explain it better than Derek Gourlay... Basically, use git pull --rebase instead of git pull :) What's missing in the article though, is that you can enable it by default:

    git config --global pull.rebase true
    

    git rerere

    Again, nicely explained here. But put simply, if you enable it, you won't have to resolve the same conflict multiple times anymore.

    0 讨论(0)
  • 2020-11-21 12:25

    TL;DR

    If you have any doubt, use merge.

    Short Answer

    The only differences between a rebase and a merge are:

    • The resulting tree structure of the history (generally only noticeable when looking at a commit graph) is different (one will have branches, the other won't).
    • Merge will generally create an extra commit (e.g. node in the tree).
    • Merge and rebase will handle conflicts differently. Rebase will present conflicts one commit at a time where merge will present them all at once.

    So the short answer is to pick rebase or merge based on what you want your history to look like.

    Long Answer

    There are a few factors you should consider when choosing which operation to use.

    Is the branch you are getting changes from shared with other developers outside your team (e.g. open source, public)?

    If so, don't rebase. Rebase destroys the branch and those developers will have broken/inconsistent repositories unless they use git pull --rebase. This is a good way to upset other developers quickly.

    How skilled is your development team?

    Rebase is a destructive operation. That means, if you do not apply it correctly, you could lose committed work and/or break the consistency of other developer's repositories.

    I've worked on teams where the developers all came from a time when companies could afford dedicated staff to deal with branching and merging. Those developers don't know much about Git and don't want to know much. In these teams I wouldn't risk recommending rebasing for any reason.

    Does the branch itself represent useful information

    Some teams use the branch-per-feature model where each branch represents a feature (or bugfix, or sub-feature, etc.) In this model the branch helps identify sets of related commits. For example, one can quickly revert a feature by reverting the merge of that branch (to be fair, this is a rare operation). Or diff a feature by comparing two branches (more common). Rebase would destroy the branch and this would not be straightforward.

    I've also worked on teams that used the branch-per-developer model (we've all been there). In this case the branch itself doesn't convey any additional information (the commit already has the author). There would be no harm in rebasing.

    Might you want to revert the merge for any reason?

    Reverting (as in undoing) a rebase is considerably difficult and/or impossible (if the rebase had conflicts) compared to reverting a merge. If you think there is a chance you will want to revert then use merge.

    Do you work on a team? If so, are you willing to take an all or nothing approach on this branch?

    Rebase operations need to be pulled with a corresponding git pull --rebase. If you are working by yourself you may be able to remember which you should use at the appropriate time. If you are working on a team this will be very difficult to coordinate. This is why most rebase workflows recommend using rebase for all merges (and git pull --rebase for all pulls).

    Common Myths

    Merge destroys history (squashes commits)

    Assuming you have the following merge:

        B -- C
       /      \
      A--------D
    

    Some people will state that the merge "destroys" the commit history because if you were to look at the log of only the master branch (A -- D) you would miss the important commit messages contained in B and C.

    If this were true we wouldn't have questions like this. Basically, you will see B and C unless you explicitly ask not to see them (using --first-parent). This is very easy to try for yourself.

    Rebase allows for safer/simpler merges

    The two approaches merge differently, but it is not clear that one is always better than the other and it may depend on the developer workflow. For example, if a developer tends to commit regularly (e.g. maybe they commit twice a day as they transition from work to home) then there could be a lot of commits for a given branch. Many of those commits might not look anything like the final product (I tend to refactor my approach once or twice per feature). If someone else was working on a related area of code and they tried to rebase my changes it could be a fairly tedious operation.

    Rebase is cooler / sexier / more professional

    If you like to alias rm to rm -rf to "save time" then maybe rebase is for you.

    My Two Cents

    I always think that someday I will come across a scenario where Git rebase is the awesome tool that solves the problem. Much like I think I will come across a scenario where Git reflog is an awesome tool that solves my problem. I have worked with Git for over five years now. It hasn't happened.

    Messy histories have never really been a problem for me. I don't ever just read my commit history like an exciting novel. A majority of the time I need a history I am going to use Git blame or Git bisect anyway. In that case, having the merge commit is actually useful to me, because if the merge introduced the issue, that is meaningful information to me.

    Update (4/2017)

    I feel obligated to mention that I have personally softened on using rebase although my general advice still stands. I have recently been interacting a lot with the Angular 2 Material project. They have used rebase to keep a very clean commit history. This has allowed me to very easily see what commit fixed a given defect and whether or not that commit was included in a release. It serves as a great example of using rebase correctly.

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