I think merge commits are key to why "snapshots" are a better model than "diffs", and a stronger arguments would emphasize this more.
Like people have said, the two models:
- a commits is a snapshot plus a pointer to a parent commit
- a commits is a pointer to a parent commit plus a diff
are sort of isomorphic. And some commands in the git porcelain (like git cherry-pick, or git rebase) indeed make more sense if you think of commits as diffs.
But this isomorphism becomes really strained when you have commits with more than one parent (or even zero parents). (And I think it's telling that those commands don't play very nicely with merge commits or the root commit.)
If you really want to incorporate merge commits and the root commit, the alternatives become:
- a commit is a snapshot, together with a list of zero or more pointers to parent commits
- a commit is a list of M >= 0 pointers to parent commits, together with N > 0 diffs, subject to the invariant that:
a) M = N, except that for exactly one commit, which we will call the "root" we are allowed to have M = 0 but N = 1
b) starting from any commit, if you traverse a path back to the root commit by following parent pointers, and then sequentially (in reverse order) apply, for each commit in the path, the diff that corresponds to the parent pointer chosen, then the result of composing all those diffs is independent of the path chosen.
And when you put it like that, it's pretty clear that the "diffs" model is really impractical, and that's why it's a lot better to think of commits as snapshots.
Like people have said, the two models:
- a commits is a snapshot plus a pointer to a parent commit
- a commits is a pointer to a parent commit plus a diff
are sort of isomorphic. And some commands in the git porcelain (like git cherry-pick, or git rebase) indeed make more sense if you think of commits as diffs.
But this isomorphism becomes really strained when you have commits with more than one parent (or even zero parents). (And I think it's telling that those commands don't play very nicely with merge commits or the root commit.)
If you really want to incorporate merge commits and the root commit, the alternatives become:
- a commit is a snapshot, together with a list of zero or more pointers to parent commits
- a commit is a list of M >= 0 pointers to parent commits, together with N > 0 diffs, subject to the invariant that:
a) M = N, except that for exactly one commit, which we will call the "root" we are allowed to have M = 0 but N = 1
b) starting from any commit, if you traverse a path back to the root commit by following parent pointers, and then sequentially (in reverse order) apply, for each commit in the path, the diff that corresponds to the parent pointer chosen, then the result of composing all those diffs is independent of the path chosen.
And when you put it like that, it's pretty clear that the "diffs" model is really impractical, and that's why it's a lot better to think of commits as snapshots.