9 ms·
From a storage perspective, describing commits as snapshots seems like a bad mental model. Suppose I have a directory that is 100MB in size. If I take a snapsho
by whack 6y ago
From a storage perspective, describing commits as snapshots seems like a bad mental model. Suppose I have a directory that is 100MB in size. If I take a snapshot of it, my snapshot would be 100MB in size. If I take a 2nd snapshot of it tomorrow, my 2nd snapshot would also be 100MB in size. My total storage needs would now be 300MB.
Whereas if I had used git, and created 2 additional commits, each making a change to a small text file, my total storage size would be barely larger than 100MB. Describing the commits as a diff, as opposed to a snapshot, leads to a better intuitive understanding of why this would be the case.
Not to mention other features the article discussed, such as cherry-picking. What does it even mean to "cherry-pick a snapshot"? In comparison, cherry-picking a diff and applying it to your current state, is far more intuitive.
And let's not forget commit messages. If a commit is a snapshot, I would expect the commit-message to be descriptive of the entire snapshot. Whereas if a commit is a diff, I would expect the commit message to be descriptive of the diff. Which is exactly how most people use commit messages.
Obviously both "diffs" and "snapshots" are leaky abstractions. If you insist on using the "snapshot" abstraction, you will need to resolve all of the above points of confusion by adding more complexity to your abstraction. And if you prefer to use the "diff" abstraction, you will eventually need to explain that a commit is actually a combination of diffs, along with some other metadata like a pointer to a parent commit. As a teaching tool, you can make either abstraction work. But I find it far more intuitive and useful to think of commits as "diffs + some metadata".
- dbt00 6y agoif your filesystem was copy on write and implemented snapshot semantics internally (like WAFL for example, over 20 years old now), then the second snapshot would not take 100MB, it would just cost the metadata. A commit is a snapshot of a tree with a reference to it's prior ancestors. It's important to know that because it becomes extremely relevant when trying to do things like merges properly.
- towergratis 6y agoLookup Copy-On-Write. ZFS and BtrFS do it.
- jayd16 6y agoDepends on the diff. If the diff is not aligned by bits a single bit offset might cause double the size, ie the full file to delete and a full file add. >If you insist on using the "snapshot" abstraction But its not insisted. Both abstractions are used as needed.
- goerz 6y agoYou can still think of them as snapshots. Git just does compression on the entire folder of snapshots, including de-duplication of data that doesn't change between snapshots. In fact, when I teach git to students, I don't even bother with the trees/blobs, which in my view are just an implementation detail. I just tell them to think of git zipping up their working directory together with some metadata (commit message, reference to parents), and putting that zip file into its own "compressed" storage inside the .git directory. That seems to be sufficient for a good mental model of how to work with git (independently of the git's somewhat baroque command line interface, which just takes getting used to)
- gilbetron 6y agoSo it only stores the difference between the two snapshots? ;)
- dbt00 6y agoThe implementation-specific compression doesn't store deltas or diffs, it stores unique blocks of text. Git allows for shallow clones, which would be impossible if the protocol or implementation were based solely around diffs.
- detaro 6y agoNo. If it chooses to compress the commits, which it if I remember correctly not does automatically for each commit, but rather occasionally as a larger step, it uses the difference to whatever it deems to be a good candidate, if it finds one. E.g. if you have a file in commit A, change it massively in later commit B, and then on a different branch create commit C that also changes the file to one very similar to the one in B, git might very well compress C by storing the difference from B to C, despite those having no direct relationship in the commit graph. It can also choose to not use a delta to a different version entirely, and this is 100% an internal implementation detail of the storage system in git (afaik one of those implementation details is that it prefers candidates that are in the same commit chain, but it doesn't have to - and it can easily jump multiple commits if that works better). If you ask git to show you a diff to the previous commit, it does not pull a diff from storage, but pulls two file versions from its storage backend (which if deltas have been used to store will resolve those) and diffs them.
- cesarb 6y ago> From a storage perspective, describing commits as snapshots seems like a bad mental model. Suppose I have a directory that is 100MB in size. If I take a snapshot of it, my snapshot would be 100MB in size. If I take a 2nd snapshot of it tomorrow, my 2nd snapshot would also be 100MB in size. My total storage needs would now be 300MB. That's not what one would expect. Suppose I have a directory that is 100MB in size. If I take a snapshot of it ("btrfs subvolume snapshot"), my snapshot would be 100MB in size, but the storage needed for the original and the snapshot together would still be 100MB (plus a few kilobytes of overhead). If I take a second snapshot of it tomorrow ("btrfs subvolume snapshot" again), my second snapshot would also be 100MB in size, and my total storage needs would still be 100MB (plus a few kilobytes of overhead). If I made a change to a small text file before each snapshot, my total storage size would still be barely larger than 100MB. That is, when creating a snapshot, one would expect it to be copy-on-write. While not exactly what git does (it's a content-addressable storage instead of a copy-on-write storage), the end effect is similar enough for most purposes (the main difference being that undoing a change in git would not need extra storage, while a copy-on-write storage would store a new copy of the contents).
- whack 6y agoI've learnt something new today, thanks for sharing. Looks like I had a naive understanding of how snapshotting actually works. I still think that it's more intuitive to describe commits as diffs, in the context of things like cherry-picking a commit or rebasing/reordering a series of commits. But given that you can also "check out" a commit, in order to get a specific snapshot of the repo, I can see the parallels between commits and snapshots. Maybe both analogies are equally useful in describing the different features that git provides.
- diroussel 6y agoThe point of the article is not an analogy. Git is based on snapshots. Abs diffs are computed from snapshots as needed. The snapshots are also de-duplicated and compressed, but that is not important. The article is a good one. And if you spend the time to understand git it gets easier to use.
- Ericson2314 6y ago> Obviously both "diffs" and "snapshots" are leaky abstractions..... Joel Spolsky wrote many great things, but "all abstractions leak" was not one of them (edit his but not good). I am very tired of programmers excusing their poor imagination with appeals to this nonsense. ------ Commits store snapshots. Full stop. The "bad mental model" is not commits being snapshots, but things behind stored individually, i.e. > Sum |things| = |Product things| This comes up in many other contexts, especially when storage quotas are involved and it's unclear what to do when storage is deduped across quotas. ----- git packfiles do use a delta encoding, but it's important to understand that there isn't any necessarily any correspondence between the history and the delta encodidng. In fact, commands like `git repack` exist precisely to avoid path dependency issues from the repacks matching the history too much. Saying commits are diffs to explain the delta-encoding storage characteristics is wrong and confuses, not clarifies. ------ > And let's not forget commit messages. If a commit is a snapshot, I would expect the commit-message to be descriptive of the entire snapshot. Whereas if a commit is a diff, I would expect the commit message to be descriptive of the diff. Which is exactly how most people use commit messages. It's git tree objects that are snapshots, commit objects have tree child and a prev commit child, so it is natural for them to describe the relationship between two states without appealing to hypothetical alternatives. > Not to mention other features the article discussed, such as cherry-picking. What does it even mean to "cherry-pick a snapshot"? In comparison, cherry-picking a diff and applying it to your current state, is far more intuitive. I might `git checkout somethingelse .` mid-rebase. What does that mean if commits are diffs? Nothing very clear. The better thing to teach people is about darcs and patch theory and those other modules. I think the git model and the patch theory model both have uses, and the fact that git makes people always work in the git model is a fundamental issue that cannot be fixed with analogies. - Patch theory is good for the things are you still working on - merkle dag of states is good for the things you've already done / agreed upon.
- gowld 6y ago> All non-trivial abstractions, to some degree, are leaky. You look a bit silly making grandiose comments that take one web searching to disprove https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/ https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-a... > All non-trivial abstractions, to some degree, are leaky.
- LukeShu 6y ago> Suppose I have a directory that is 100MB in size. If I take a snapshot of it, my snapshot would be 100MB in size. Not with `btrfs subvolume snapshot`, it won't. If that's not a snapshot, I don't know what is. From a storage perspective, no dammit, Git commits are snapshots, look at the bits on disk if you don't believe it. This isn't something that people who like to write blog posts about Git made up for pedagogical purposes, it's how Git actually works. As you point out, it's wonky for pedagogical purposes; what does it mean to "cherry-pick" a snapshot? When thinking about cherry-picking, yeah, a diff makes more sense than a snapshot. But saying a diff is better pedagogically doesn't change the fact that a commit actually is a snapshot (and when cherry-picking, it diffs to snapshots to create a patch, then applies that patch).
- jgraham 6y ago> From a storage perspective, no dammit, Git commits are snapshots, look at the bits on disk if you don't believe it Except they're not. They're (often) packfiles, which are a delta encoding i.e. a diff. It's not necessarily the same as a specific commit, but appealing to "the bits on disk" is wrong. It is certainly true that the git object model each commit object refers to a tree that represents the complete state of the repository at that commit. It is also true that many git commands implictly treat a commit as being the diff between the state of the tree in that commit and the state in the parent. For example git show, git rebase and git cherry-pick. It is simultaneously true that the on-disk storage system is optimised for performance and so doesn't map onto the object model in a trivial way.
- nyanpasu64 6y ago> It is also true that many git commands implictly treat a commit as being the diff between the state of the tree in that commit and the state in the parent. For example git show, git rebase and git cherry-pick. A commit is a snapshot, and you can compute the diff between a commit and any of its parents. If a commit has multiple parents, git cherry-pick bails out unless you pick a parent (usually -m 1), and git rebase, I think implicitly assumes the first parent. (EDIT: a commit's tree, its parents' trees)
- outworlder 6y agoCommits are snapshots. How to represent those snapshots, and fix the storage bloat a naive implementation would cause, is a completely different problem. One of the things that makes Git smart is that it doesn't try to optimize things prematurely. SVN and co. would store actual diff data, but this made some operations really hard to implement (and, in many cases, slow). Git has commits conceptually as snapshots. It's up to the storage code to figure out how to deal with this. > But I find it far more intuitive and useful to think of commits as "diffs + some metadata". Except that this is not what's happening. I wouldn't even call it an abstraction, it's how things actually work. What you call abstractions are actually operations. If we run a diff we are interested in the changes, but if you ask git to show you the commit it will show you just that. If you think a commit is a diff, you have a mismatch between the mental model and what's actually happening behind the scenes. This will make it difficult to understand concepts later on.
- slavik81 6y ago> If we run a diff we are interested in the changes, but if you ask git to show you the commit it will show you just that. git show <commit SHA-1> will output a diff.
- trulyme 6y agoI think this is more a sign that git (porcelain) is not aligned with the underlying model. It is actually a pity that so little effort went into git UI. I find the OP explanation of git model awesome and the presented concepts beautiful, but the cli utility has countless naming and consistency problems which make me sad that hg didn't win over git. Life would be much simpler for many developers if it did, imho.
- munk-a 6y ago> If you think a commit is a diff, you have a mismatch between the mental model and what's actually happening behind the scenes. This will make it difficult to understand concepts later on. I don't think those concepts are distinct as you're painting them. At a user visible level commits will almost always be visualized as diffs, which puts us at a place where - at the highest level and lowest level they're defined as pretty close to diffs, while at an intermediary level they're defined closer to snapshots. I honestly think they're neither, each expression method (diff vs. snapshot) can be translated pretty easily and both are trying to represent the same end goal. It can be helpful to know that commits are representative of the full state of the codebase that exists at a time, but that view can be at odds with merging and rebasing which use actual change sets to calculate - when a commit is being manipulated it's helpful to view it as a diff (and git does this) - while as, when a commit is being read, we're using it as a snapshot.
- kazinator 6y ago> What does it even mean to "cherry-pick a snapshot"? It means to do something like a three-way diff among three snapshots: the cherry-picked baseline, the target, and a common ancestor. You can do something similar with the diff3 tool, which takes three files (snapshots) as input, not diffs.
- jedimastert 6y agoI prefer to think of a repo as a whole as a tree, where the nodes are snapshots and the vertices between each node is a diff. This sort of lands us in both places
- Twirrim 6y ago> From a storage perspective, describing commits as snapshots seems like a bad mental model. Suppose I have a directory that is 100MB in size. If I take a snapshot of it, my snapshot would be 100MB in size. If I take a 2nd snapshot of it tomorrow, my 2nd snapshot would also be 100MB in size. My total storage needs would now be 300MB. That's not the way storage snapshot works under most (all?) storage targeted file systems, filers etc.. What you're talking about there is a backup. Snapshots are not backups. Snapshots work on "copy on write" basis. Roughly speaking, when you take a snapshot you draw a line in the sand. "These were the files at this time". Snapshot operations as a result are super cheap and super fast. Future changes to those files results in the filer/file system writing the modified blocks to new locations, not overwriting the original data. So take a 100MB directory. I create a snapshot. That results in almost new storage usage, just a small amount of metadata. I write/modify 10MB of data, now the total storage cost is 110MB. If I take another snapshot after writing that 10MB. it's still only 110MB of storage usage.
- NTARelix 6y agoAfter going through the "Git Internals"[0] docs, I found that the snapshot mental model has been much more helpful in understanding what my Git commands are doing, how someone's history got into a confusing state, etc. The primary model is that of the Merkle tree, and subsequently hashing, which are very simple and powerful concepts. [0]: https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
- dfox 6y agoIf you commit 100MB file, change few bytes in it and commit it again your .git/objects will almost certainly contain two 100MB objects. The fact that it is somewhat likely that running "git gc" or something similar will convert one of them into reference to the other one and some compact representation of the difference is implementation detail. While commit object does represent the snapshot it also references the previous state, thus the commit message usually describes what was changed between the referenced snapshot and the parent(s) that are also referenced from the commit object. As for the overall model and leakage between implementation details and how people use it interesting approach is used by SCCS/BitKeeper with its internal "weave" format that essentially is both snapshot and diff at the same time.
- TeMPOraL 6y agoIf "diffs" and "snapshots" are leaky abstractions, that often enough lead you badly astray, then why insist on these abstractions in the first place? Why not just teach people the mental model behind Git up-front? Objects form an immutable directed acyclic graph, human-readable names point at objects, there are some rules by which the graph is being extended and pruned, and by which names (references) are being updated to point at different objects. This isn't a hard mental model, not for programmers (for whom the tool is intended in the first place). If you know how the most basic pointer-based data structures - a linked list, a tree, a directed graph - work, then learning the actual model isn't hard, immediately clarifies why Git does what it does. It should be taught to people up front. A commit isn't a diff, and it isn't a snapshot. It's a bunch of objects Git creates for you, where the "commit" object points at previous commits and at a tree, built of "tree" and "blob" objects. When Git wants to know how to recreate your file structure, it starts at the "commit" object and walks the graph to discover what files and folders should exist. When you make a change and perform the "commit" action, Git creates a new "commit" object and a new "tree" object for it, and add more objects to the graph to encode what changed, while reusing previously existing objects for things that did not change. The end state is, if you start at your new "commit" object and walk the graph, the resulting description of your file structure should be equal to what's on your hard drive when you made your commit. Trying to paper over that with "friendly abstractions" is what makes Git difficult to understand.
- still_grokking 6y ago>… you will eventually need to explain that a commit is actually a combination of diffs, along with some other metadata like a pointer to a parent commit. Only that this is completely wrong. A commit is a snapshot of the tree. There are no diffs. There is also no "metadata attached" — the commit is the actual data (!) describing the tree snapshot. Git is a kind of simple content addressable object store storing kind of Merkle tree objects. That would be a proper (abstract) description.