Combine or rebase an arbitrarily large number of commits

前端 未结 3 863
伪装坚强ぢ
伪装坚强ぢ 2020-12-28 11:46

Let\'s say my local git log shows:

739b36d3a314483a2d4a14268612cd955c6af9fb a
...
c42fff47a257b72ab3fabaa0bcc2be9cd50d5c89 x
c4149ba120b30955a92         


        
相关标签:
3条回答
  • 2020-12-28 11:57

    Sometimes, You Might Not Want to Use Interactive Rebase

    If the number of intermediary commits between A through X is relatively small, you could get by just fine by using interactive rebasing:

    git rebase -i origin/master
    

    However, in my personal experience, using interactive rebasing on a large number of commits is slow. I've run an interactive rebase on about a hundred commits at once (on a Windows machine using Git Bash), and it took a long time for msysgit to generate the interactive rebase commit editor that allows you to select which operations you want to run on which commit, because, well, the list ended up being very large.

    In such a situation, you have a couple of workarounds.

    Alternative #1: git reset

    Mixed and soft resets can be used to modify your working-tree or staging area (respectively) to aggregate/collect all the changes between two commits A and Y, after which you can commit all of the modifications at once as a single commit.

    I will only give an example of a soft reset, since it already leaves everything staged for you, while if you use a mixed reset, you'll have to stage the modifications anyways:

    # You don't need to create the temp branch A if you
    # copy the commit sha A down somewhere so that you can remember it.
    git branch temp A
    
    git reset --soft Y
    git commit -m "Squash commits A through X"
    
    # Verify that this new commit is equivalent to the final state at A
    git diff A
    

    Alternative #2: Using Patches

    Another option is to simply use patches. Just generate a diff patch of the difference between A to Y, then apply the patch as a new commit on top of Y:

    git diff y a > squash.patch
    git checkout -b squash-branch y
    git apply squash.patch
    git commit -m "Squash commits A through X"
    
    # Verify that this new commit is equivalent to the final state at A
    git diff A
    

    As pointed out by @A-B-B in the comments, this won't quite work if there are binary files involved. git diff --binary can be used to output a diff for binary files in addition to the text files, but I'm not sure if those diffs can then be used as patches as well.

    0 讨论(0)
  • 2020-12-28 11:59

    One option is git rebase -i @{u}. I use this frequently enough that I've aliased it as git freebase (since it works on the commits that you can freely rebase).

    In case you're not familiar, @{u} is a shortcut for @{upstream}, or "the upstream of the current branch".

    0 讨论(0)
  • 2020-12-28 12:07

    "Easiest" is always a bit tricky. Interactive rebase will let you squash everything, and is "easy" by some measure.

    Another "easy" way, which looks a bit complicated, is to use a "squash merge" (which is not an actual merge at all, but does use the same underlying code as git merge, so it's done with the same command). Let's say you're on branch devel whose upstream is origin/devel. First we'll rename devel to devel-full to indicate that it's the one with the full sequence of commits. Then we'll create a new devel tracking origin/devel, and "squash-merge" devel-full:

    git branch -m devel devel-full
    git checkout --track origin/devel
    git merge --squash devel-full
    git commit
    

    You have to separately git commit as --squash inhibits the commit that git merge normally makes.

    Yet a third easy (?) but slightly scary way is to check out the tip version and commit it. Again, assuming devel as before, we move the "full develoment" branch-name out of the way and make a new local branch to make the new commit on. This time instead of git merge --squash, though, we use two commands before git commit:

    git branch -m devel devel-full
    git checkout --track origin/devel
    git rm -rf .                      # assumes you're in the top directory
    git checkout devel-full -- .
    git commit
    

    The git rm -rf . schedules (in the index/staging-area) every single file to be removed, but then the git checkout devel-full -- . tells git to re-populate the index/staging-area with every single file that exists at the tip of devel-full. So this means "make the tree for the next commit, look exactly like the tree for the tip of devel-full".

    (The remove-and-re-create method works for one case where merge --squash does not: specifically, it works for "replacing" the tip of one branch with the tip of another, even if the two branches are not related and hence not merge-able. Otherwise merge --squash is one step shorter, and definitely not as scary looking, at least!)

    Both of these other "easy" (?) ways leave you with a branch with the full development history. If you want it, great! If not, you have to delete it.

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