Skip to content

Instantly share code, notes, and snippets.

@jamesarosen
Last active January 13, 2016 00:14
Show Gist options
  • Save jamesarosen/f0b0be72b0bcf86e6220 to your computer and use it in GitHub Desktop.
Save jamesarosen/f0b0be72b0bcf86e6220 to your computer and use it in GitHub Desktop.
Trying to excise some commits

I have a project with a long git history. I want to open-source it, but there's a long section at the beginning that I want to remove first.

*    ddfe660 - (HEAD, origin/master)
|
|    (many hundreds of commits, including merges)
|
*    420b801 - (the intended beginning of the new history)
*    00a3516 - (the most recent commit I want squashed out)
|
|    (many hundreds of commits, including merges)
|
*    6b6fc1e - first commit

I pseudo-git, I want git squash 6b6fc1e..420b801. That is, I want a new first commit that is the sum of that whole range, with the commit message from 420b801.

Here are some things I've tried:

  1. Interactive rebase: git rebase -i 6b6fc1e, using fixup for all of 6b6fc1e..00a3516 and squash for 420b801. This failed as soon as I got to a merge commit in the first range because I don't know how to resolve those conflicts.
  2. Interactive rebase with --preserve-merge: git rebase -p -i 6b6fc1e. This had the same problem.
  3. git rebase -p --root 420b801. This had the same problem.
  4. Fiddling with --root: git checkout 6b6fc1e && git reset --soft 420b801 && git commit --amend, as suggested here. That didn't seem to remove any of the commits in the range I want to squash.
@wagenet
Copy link

wagenet commented Jan 12, 2016

I'm certain there's a more elegant way to do this. However, one way that would probably work is as follows. (This is off the top of my head so I might have mistakes.)

  1. Make a branch from the last commit in the range you want to squash.
  2. In this branch git reset FIRST_COMMIT. This should leave you with all the changes made between those two commits but unstaged.
  3. Make a new commit with all of these changes. If this went right, what was originally a ton of commits should just be one or two.
  4. Rebase master (or better yet make another branch from master) on this branch.

@jamesarosen
Copy link
Author

@wagenet part one worked great:

git checkout -b excise # start a new branch
git reset --hard 420b801 # point it to the new first commit
git reset 6b6fc1e # stage the whole history
git commit -m "new first commit"
git tag tmp

That left me with a new tree with exactly one commit containing exactly what I want!

Then I tried part two:

git checkout -b new-master
git reset --hard origin/master
git rebase --onto tmp

I can't exactly explain what that did, but it seemed to have made master point to just the one-commit tree tmp.

Another attempt:

git checkout -b new-master
git reset --hard origin/master
git rebase tmp

This had the merge-conflict problems.

But a third seemed to go better:

git checkout -b new-master
git reset --hard origin/master
git rebase -p tmp

I need to do some verification, but this seems to have worked!

@jamesarosen
Copy link
Author

Nope, I'm wrong. The third was the same as the first. I lost 420b801.. ddfe660. Maybe I can cherry-pick them now?

@jamesarosen
Copy link
Author

Next attempt:

git checkout -b new-master
git reset --hard origin/master
git rebase -p -i 6b6fc1e

and then remove all of the lines of 420b801.. tmp and replace them with the single line pick tmp. I still get merge conflicts, but many fewer and more recent, so there's a hope I could resolve them.

@wagenet
Copy link

wagenet commented Jan 12, 2016

Don't do a hard reset. The hard reset will remove all the changes, which you don't want to do. The git reset without hard will change the HEAD and unstage all the changes.

@jamesarosen
Copy link
Author

Sorry, I don't understand. I want to retain all the recent history. I don't just want to end up with a one-commit tree.

@jamesarosen
Copy link
Author

Now trying git rebase -i -s recursive -X theirs 6b6fc1e and replacing the first section with pick tmp...

That didn't work because git told me it didn't understand the theirs strategy.

@jamesarosen
Copy link
Author

Just tried

git checkout tmp
git cherry-pick 420b801.. ddfe660

Which kept telling me it couldn't cherry-pick merge commits without an -m. Passing that to the range didn't work. I needed to do it at each merge. That's a lot of steps, so I wrote a Ruby script:

require 'open3'

REGEX = /Commit ([0-9a-f]+) is a merge but no -m option was given\./

def step
  puts 'git cherry-pick --continue'
  out, err, status = Open3.capture3('git cherry-pick --continue')
  md = REGEX.match(err)

  return false if md.nil?

  puts "git cherry-pick #{md[1]} -m 1"
  `git cherry-pick #{md[1]} -m 1`

  puts "git commit --allow-empty -C #{md[1]}"
  `git commit --allow-empty -C #{md[1]}`

  true
end

needs_another = true

while needs_another
  needs_another = step
end

But that just got me to a merge conflict it couldn't resolve.

@jamesarosen
Copy link
Author

What ended up working:

$ git checkout master
$ git checkout -b new-master
$ git rebase -p -i --root

Then delete all the lines I wanted to remove and add pick tmp at the top. When there were rebase conflicts, they would look like

CONFLICT (rename/delete): app/pods/components/nav-item/component.js deleted in HEAD and renamed in a581711d5119e24f2e26e65795a7c2b776efaa46. Version a581711d5119e24f2e26e65795a7c2b776efaa46 of app/pods/components/nav-item/component.js left in tree.
Automatic merge failed; fix conflicts and then commit the result.
Error redoing merge 150b353693d1f31906f085acad17546128ae41a0
$ git add app/pods/components/nav-item/component.js
$ git rebase --continue

For each of these, I just did

$ git checkout 150b353693d1f31906f085acad17546128ae41a0 -- .
$ git add .
$ git rebase --continue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment