Ground of being of your question: you have
A - B - C - D - E
But you want
A - E
Fine. Let's start by clearing up some misconceptions in your question.
Commits are immutable
Commits are not just snapshots. They are also immutable. A commit different in any way from E would not be E. It might have the same commit message as E, but if it differs at all from E it is a different commit.
Parentage is part of a commit
Well, I just said it, but I'll say it again. Commits are not just their contents. They contain a bunch of other stuff, including, in particular, the information as to who their parent is.
Ok. Now, in your diagram, D is the parent of E. A commit that did not have D as its parent would have a different parent. And we have just said that commits are immutable. Therefore, if we imagine E with A as its parent, that would not be E. Again, we might call it "E" in a kind of informal way; it might have the same commit message as E did; but it would be a different commit.
No cutting and pasting
The above is sufficient to explain why you cannot merely "cut" B, C, and D, and then "paste" E to go after A. You cannot "paste" E anywhere! There is only one E — not just in your repo but in the entire universe — and D is its parent, and that's the end of that.
Therefore in order to eliminate B, C, and D from the visible history, we must somehow rewrite E to make a different commit, one that has A as its parent. And that is the must basic reason why rebasing (or something similar) is necessary in order to generate the history you want. The history
A - E
is impossible. What you can have is
A - E'
where the second commit, whose name is read "e-prime", bears some similarity to E in your mind, but is not E. And you need a coherent way to make that. That is why you must do some kind of dance in order to change history.
But that dance need not be rebasing — as we shall now see. Sip your coffee and let's go on.
No rebase necessary
Now let's say that what you mean by your question is this: B, C, and D were steps along the way to the golden truth, but E contains that golden truth and I don't really need to "show my work" to my colleagues and the world. So I'd like to hide the intermediate steps and just go from A to E — sorry, to E' — as a pure and simple statement of what happened.
Then you don't actually need to rebase. Just say
git reset --soft <SHA of A>
git commit -m 'message identical to the E message'
That will result in your desired
A - E'
What did we just do? We started with your own stated fact: a commit contains a snapshot. When we said reset --soft
, we basically produced the contents of that snapshot into both the working tree and the index. So then, when we made a new commit, that commit was a snapshot of the project in exactly the state that E had described it. But at the time we made this new commit, the HEAD was A. So the parent of this new commit is A! Problem solved.
So yes, we could have rebased and squashed the intermediate commits to get the same result. But that is just a convenient way for people who don't know how, or can't be bothered, to accomplish the same thing more directly. Git interactive rebase is an intelligent and convenient shorthand for certain common kinds of history transformation; but it does nothing you could not have done yourself, in some other way.
No rebase necessary, ever
I just want to impress upon you that you never need rebase. Everything it does can be done by a series of sometimes tedious and elaborate steps, much more basic and direct and (probably) inconvenient.
For example, suppose you wanted to eliminate B and C but keep D and E (well, actually D' and E', as we already know — D would be replaced by D' because the new commit's parent is A, not C, and E would be replaced by E' because the new commit's parent is D', not D).
You can do it without interactive rebase. Start a new branch at D. Reset that branch soft back to A and commit with D's commit message, just as we did before. Now cherry pick E onto the end of this new branch and give the resulting commit E's old message. Now clean up the branch situation, and you're done.
I'm not saying that's easier than interactive rebase; it's obviously not. But that's not the point. The point is that interactive rebase is just a crutch. A really great crutch! But it isn't magic.
Drop is not squash
Finally, you said something in your question that was very wrong: you said "drop or squash". There is no "or" here: those are totally different things! Squashing maintains the contents of the last commit in the series of squashes. Dropping does not! If you dropped B, C, and D, the resulting E' would contain a snapshot that looks nothing at all like the current E!
For example, suppose B includes a new file myfile that A does not have. And suppose C and D and E all have that file too. Then dropping B would result in an E' that lacks the file myfile.
This is because, although commits are not diffs, diffs do exist (in Git's mind, not written into the history) and they are used in the process of merging. And rebasing is actually a form of merging (I don't want to get into that just now). So by dropping B, you are reversing the diff that got you from A to B. And since part of that diff is the creation of myfile, reversing that diff is a way of saying not to create myfile. And so myfile would not appear in E' when the rebase was over, even though it did appear in E.
What to squash
Last but not least: this phrase in your question was wrong too: "squash
commits B, C and D". No. To get the result you are after with an interactive rebase, you would squash C, D, and E. In other words, the pick list would look like this originally:
pick f343cc4 B
pick f750aa9 C
pick 0105b79 D
pick 46fe327 E
and you would edit it to look like this:
pick f343cc4 B
squash f750aa9 C
squash 0105b79 D
squash 46fe327 E
You would then select E's commit message as the commit message for the resulting new commit.