1

Below is a simplified commit state on my dev branch.

7f75hsyfr - changes to file1
    file1
94dfsrg43 - more changes to file2
    file2
92d7a513 - changes to file3
    file3
035a72fd - Merge branch 'master' into dev
35bcc341 - Merge branch 'master' into dev
2e05a2fc - changes to file2
    file2
60c9daaf - changes to file1
    file1

Since I was switching between branches I was committing unfinished work which resulted in multiple commits related to the same files. There are also merge commits from merging changes from master. How can I clean things up before pushing to master branch? Ideally I would want to re-organize this so there is one commit for each file1 and file2.

marcin_koss
  • 5,763
  • 10
  • 46
  • 65
  • Here is a really great post on changing git history: https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History I've been using this as my guide when I want to perform surgery on my commits. – jaydel Jul 27 '16 at 14:03

3 Answers3

3

You need the git rebase -i command. Checkout the documentation of git rebase. You can squash your commits into one commit, reword, re-order and do any kind of clean up before you push to master. Since you mentioned having merge commits, I would recommend running git config --global pull.rebase true which will ensure that in future, whenever you pull from a remote branch, your commits will be re-applied after the head of your local branch gets updated. This means, you will not end up with merge commits coming in between your commits. It also makes things easy while cherry-picking and combining this with git rebase can be very helpful when reverting changes.

Hrishi
  • 7,110
  • 5
  • 27
  • 26
2

The Short Answer

If you didn't have the merge commits, you could just simply use an interactive rebase, as pointed out in Combining multiple commits before pushing in Git.

You can get rid of the merge commits as well by simply rebasing or cherry-picking the commits unique to your branch on top of master, since the final state of your code will be equivalent to having done a merge.

To cleanup your branch, the following solution will combine rebasing and cherry-picking commits to remove the merge commits, then interactive rebasing to squash the others.

Rebase and Cherry-pick Commits On Top of Master

We'll create a temporary branch to keep the current state of dev, then rebase the parts of dev before the merges with master on top of master, which is equivalent to having merged master in the first place:

# Create temporary branch to save current state of dev
git branch temp dev

# Temporarily reset dev to state before the merges
git reset --hard 2e05a2fc 

# Rebase dev onto master
git rebase master

Next, cherry-pick the remaining commits from temp. We'll use a commit range, where the start of the range is exclusive (i.e. is not actually included):

git cherry-pick 035a72fd..7f75hsyfr

Squash Commits with Interactive Rebase

Now you're ready to squash commits with interactive rebase. Find the rewritten commit that corresponds to 60c9daaf in your example. Let's call that commit X. Then

git rebase -i X^

X^ represents the base commit of the rebase, which won't actually be changed. Every descendant commit, however, will not appear in the interactive rebase editor, in order of oldest to newest:

pick X  changes to file1
pick Y  changes to file2
pick Z  changes to file3
pick AA more changes to file2
pick AB changes to file1

Reorder the commits so that commits changing a file are adjacent

pick X  changes to file1
pick AB changes to file1
pick Y  changes to file2
pick AA more changes to file2
pick Z  changes to file3

Then add squash or s to each following commit that you want to squash into the previous one:

pick X  changes to file1
squash AB changes to file1
pick Y  changes to file2
squash AA more changes to file2
pick Z  changes to file3

When you're done, save and exit the editor, and the rebase will continue unless there are conflicts, in which case you'll need to resolve each conflict and continue the rebase.

Verify Results

Now you'll want to verify your results to make sure that the changes you've made are still equivalent to the state of your branch before you started to make modifications:

git diff temp

If there is no output, then the final state of your branch is the same as before you started modifying it, so now you may delete temp with git branch -D temp.

See Also

Community
  • 1
  • 1
1

I don't think there's a particular reason to do this - normally you'd keep all the commits intact. However, if you want to do this, you could create a patch file and apply that on the master. I usually use gitk to create patches, since it makes it pretty easy to pick the start and the end, but there's a command to do this, too.

Edit:

The command-line solution would be something like:

git checkout <branch>
git format-patch master --stdout > ../blahblah.patch
git checkout master
git apply ../blahblah.patch
git add .
git commit . -m"apply changes from branch"

That would put all the changes from the branch in one commit, but you could cherry-pick commits and bundle them as you choose.

Don Branson
  • 13,631
  • 10
  • 59
  • 101
  • There is a really solid use case for doing this (in my own workflow). If I've got a ton of commits in my PR and I want to isolate and group the logic in order to make life easier on the PR reviewer. For example, I end up adding columns to a table across several commits and those commits include things unrelated to the db change, I can split the commits in question, and isolate the db commits, then I can reorder those commits and squash them. I only do this on my private feature branch, never on a public branch. History is entirely relative. also I use `git rebase -i` exclusively for this. – jaydel Jul 27 '16 at 14:02