3

My Problem

I have been working on a git repo. Before publishing my changes, I'd like to remove some commits - a023b43, 315424b and 7b51754 - as they do not reflect any meaningful changes. The only important points in time are the first (70f72ca) and last (6937ddd) commits.

What Have I Tried

git rebase -i 4c802c1

I chose:

pick 70f72ca Remove repetitions from Makefile
d a023b43 Separate target for image conversion
d 315424b Remove image conversion (#1)
d 7b51754 Fix Makefile
pick 6937ddd CV 2019.01

The final goal is to have a git log that looks like:

6937ddd CV 2019.01
4c802c1 Initial commit

What Did not Work

Auto-merging src/Adam_Matan.tex
CONFLICT (content): Merge conflict in src/Adam_Matan.tex
error: could not apply 6937ddd... CV 2019.01

Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

Could not apply 6937ddd... CV 2019.01

As I understand it, the two commits are just snapshots of my working directory. I want to have two snapshots - the original commit and the CV 2019.01 one. I don't want to merge or combine them in any way. Why do I get a merge conflict message?

My Question

How can I delete all intermediate commits from a branch, and leave only the first and last commits?

Update

Squashing creates merge conflicts as well:

git rebase -i 4c802c1
Auto-merging Makefile
CONFLICT (content): Merge conflict in Makefile
error: could not apply 315424b... Remove image conversion (#1)
Adam Matan
  • 128,757
  • 147
  • 397
  • 562

3 Answers3

6

Rebase acts by taking a series of commits and re-applying them, perhaps on a different "base" object. It does either by creating a patch for each commit, and re-applying them, or by cherry-picking them in-order.

Regardless of the mechanism, by removing a commit from the list of rebase instructions, you have asked for those changes not to be included.

Imagine if you had some file, and in its initial revision, it has a single line with the contents one. Imagine that in the next commit, you change one to two. In the subsequent commit, two to three. Then three to four, and finally four to five. So you now have five commits, each changing this line.

Imagine that in each commit you gave it a message indicating the change in contents, and that magically the commit ID (hash) also reflected the contents. Running rebase -i --root would give you this script:

pick 1111111 add one
pick 2222222 change one to two
pick 3333333 change two to three
pick 4444444 change three to four
pick 5555555 change four to five

In other words, the first commit picks the change from nothing to one. The second one will cherry-pick the change from one to two, etc, until the last instruction cherry-picks the change from four to five.

If you remove some of those lines, giving you:

pick 1111111 add one
pick 5555555 change four to five

You will have two cherry-picks. The first will add a file with contents one. The second will try to change those contents from four to five.

Alas, those contents are not four. Thus, you have a conflict.

If your goal is to get from one to five with no intermediate commits, then you want to squash them or mark them as fixup commits.

pick 1111111 add one
pick 2222222 change one to two
squash 3333333 change two to three
squash 4444444 change three to four
squash 5555555 change four to five

In this case, the first change will be cherry-picked, so the file one will be created. The subsequent change will be cherry-picked, so the contents will be changed from one to two. And subsequent changes will be cherry-picked, further updating the file, but those changes will be "squashed" into the prior commit. Thus you'll get the full contents, but you'll only end up with two commits.

(Had you marked them as fixup commits, you would get the same contents, but their commit messages would not be included in your suggested commit message.

Edward Thomson
  • 74,857
  • 14
  • 158
  • 187
4

The problem is that git rebase uses contradictory terminology to how git as a whole defines commits. A commit is a snapshot, but rebase treats a commit as a patch (a set of changes) between its parent commit and itself.

So if you tell rebase to "delete a commit", it thinks you want to undo the changes applied by that commit's patch.

If you want to remove some snapshots while keeping later snapshots the same, you tell rebase to squash the commits. In rebase's terms, this means you're combining the changes from multiple commits into the latest of those commits.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
-1

For situations like this, I have to hand craft my own commits

The final goal is to have a git log that looks like:
6937ddd CV 2019.01
4c802c1 Initial commit

  1. get the full SHA1 of the first commit. You'll need this later.
    git rev-parse 70f72ca
  2. Retrieve the commit object of the last commit to a text file.
    git cat-file commit 6937ddd > commit.txt
  3. Edit commit.txt. Change the parent line from 7b51754.... to the output from 1)
  4. Write the (custom) commit object to your git database cat commit.txt | git hash-object -t commit --stdin -w
  5. Tag/branch/reset --hard the SHA1 that the above command prints out. You should see your desired history.

The reason this works is because you have already created the desired states of your repo with your current commit chain. In a commit object, the repo state is defined by the tree line, so here all you are doing is explicitly defining which repo state follows another.

Ben
  • 1,287
  • 15
  • 24