Combine or rebase an arbitrarily large number of commits

◇◆丶佛笑我妖孽 提交于 2019-11-30 02:25:12

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".

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.

"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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!