Diffs are a "natural" object for version control yet got doesn't actually use them and as we can see in this article, multiple git commands leak this implementation detail.
Diffs are a "natural" object for version control yet got doesn't actually use them and as we can see in this article, multiple git commands leak this implementation detail.
Those are snapshots of individual versions of the files, not diffs.
Or think about your favorite wiki (Wikipedia or your corporate wiki) - if you hit the "History" page, what you see is a list of the versions of the page, and then some UI to compare any two given versions. While there's a button for computing a diff between any version and the previous, that's not what it appears to treat as the natural object.
Diffs are an emergent object when you have some mechanism to automatically create them and apply them. (This is generally a programmatic mechanism, but there have long been cases where the mechanism is manual effort - think laws that amend other laws by saying "After section 3 insert... and remove section 5".) But the ultimate goal is to produce a version of the file.
If you want a diff management system, play around with quilt, which treats diffs (patch files) as the first-class object. Just about everyone I know who's tried using quilt for more than the tiniest amount of work vastly prefers just importing the applied patches into git and working with them in git. This is largely because quilt is empirically much more leaky - it's very sensitive to the current state of your directory, because a diff is a second-class object which requires something to be applied to in order to make sense, and it's too easy for the contents of your directory to not quite line up.
(Put another way - there's a reason we call it "version control" / "revision control". The thing it tracks is versions/revisions, which are concrete instances of the file in time. Quilt is a patch management system, and very few people are interested in those.)
If anything, most users I've exposed even to simple diff tools immediately appreciate the value. It's just that mainstream tools rarely support diffing since it's very complex from a technical point of view (end user tools and apps rarely work with plain text, they use binary formats, marked up text, etc), so it's not a mainstream paradigm as a result, and people are less familiar with it.
> But the ultimate goal is to produce a version of the file.
The fact that the end result is a version doesn't mean versions should be the main object. If you add a semicolon to a file, the actions you do are "press the ; key", not "create an entire version from scratch that has a semicolon there".
> If you want a diff management system, play around with quilt
You're doing it again! Why pick the least suitable tools for the jobs? Quilt is not a patch-based version control system, Pijul and Darcs are.
Diffs are natural objects for evaluating commits--answering questions like "does this new version of the code make sense?"--but they are not the natural objects for storing commits.
Diffs are "natural" objects for users of source control.
Diffs are not natural objects for storing anything since that's not the job of the users, it's the job of the version control system. It can use ponies to store the "controlled versions" for all I (and probably other users) care.
This I would like to see. :-) Particularly the part about defining a protocol for sending messages over the Internet using ponies (there already is one for pigeons...)
- Merge and rebase both try to "replay" patches. But because Git doesn't work with patches, it actually only simulates that, using heuristic algorithms such as 3-way merge. This works most of the time, but not always (see https://pijul.org/manual/why_pijul.html).
- Conflict resolutions in Git cannot be modeled as patches, and you need `git rerere` to simulate that. This is not anecdotal, since conflicts is the one situation where you need the best and most intuitive tool.
- Short-lived branches are another poor simulation of patches: if Git were really working on patches, you would be able to create a branch "after the fact", meaning that you would work (possibly on multiple features at the same time), and push different parts of your work separately, without having to worry about branches before you start.
You made me remember something I've been annoyed with version control systems. Which I've found hard to explain. But your comment makes me think about what doesn't come across.
The problem with version control systems is they are stand alone tools that don't capture and record refactoring. What you want is not a patch/diff. You want a record of the refactoring commands, and the result as separate things. And the ability to tag those.
1 Tag with meta data (commit message etc)
2 Command: Change this method name from this to that.
3 ChangeSet: List of 371 places where the change was made.
Consider if you have a couple of projects that rely on a framework/library. You could apply the commands to each project. And automagically get change sets for each.I was criticizing Git for not using diffs/patches as a regular version control user would expect.
Of course a single snapshot can't model diffs properly, since a diff is a property of a pair of snapshots.
Your examples of how git doesn't treat things properly look to me like cases where git is picking the wrong pairs of snapshots to generate diffs.
That isn't quite true, even without conflicts. The same example that shows that Git merge is not associative (https://pijul.org/manual/why_pijul.html) can be used to show that it also isn't commutative.
That is, git is content-based and not named-based. Name-based systems explicitly track renames with metadata; git does not. git calculates renames dynamically based on content.
That is true, at least for the text-based diff(1) formats, though obviously nothing precludes extending the format, or even using something else entirely (aside from the risk of not being compatible with diff(1), but then you could always present diff(1) compatible diffs externally and use something richer internally, for instance).
> git handles them better than other version control systems.
But that is not.
> That is, git is content-based and not named-based. Name-based systems explicitly track renames with metadata; git does not. git calculates renames dynamically based on content.
And as a result regularly fucks up tracking renames, or plain refuses to do it without nonsensical workarounds if you copied or moved a file and modified it in the same commit. It also makes browsing histories, blaming, or tracking merges through renames incredibly expensive.
(Although honestly I never really have problems with it messing up. If you approximately separate renaming from editing, it seems to work very reliably for me.)
There's also nothing stopping anyone from writing a git history browser that caches the calculation. The point is that it's ephemeral / derived state, not authoritative state.
One problem with the name-based systems is that developers don't actually use the VCS rename operation. They just delete and add, and then you've lost the diff under every system I know of. Another problem is importing from other VCSes -- the metadata is often messed up subtlely.
Git keeps it simple. And they can and have improved the algorithm over time without making your repo data obsolete / incompatible.
With P4, you can move the files around, adjust everything until it compiles, and only then start telling P4 about what you actually did (you can even start from Reconcile Offline Work, which will try to use exactly Git's logic to identify moved/renamed/copied files based on content - but only one time, on your own machine, not every time someone looks at the history).
That’s a matter of tooling. In languages and environments where using an IDE is the norm, use of VCS operations happens automatically, simply by virtue of performing all source file operations through the IDE.
Maybe this is because I designed that part in Pijul, but I disagree. Pijul has renames commute with other types of edits, and renames are faithfully modeled in Pijul patches, not just "guessed" after the fact based on contents.
Further, diffs only make sense if you have the full version of the files, pre-diff. Without that full file state, diffs could operate in all sorts of incorrect ways.
I think this is one thing that git actually does correctly: save full checkpoints of a directory structure. I want the same thing when I'm dealing with, say, edits of an essay or book. I want the saved full state, and may produce a diff for convenience, but I wouldn't want to save the diff as the fundamental object of interest.
This is somewhat contradictory. Of course, everybody works on states. But as you write yourself, you make changes to a file, you don't create a full new version from scratch every time you add a semicolon.
So, if you "make changes", you actually do "work on diffs".
> Further, diffs only make sense if you have the full version of the files, pre-diff. Without that full file state, diffs could operate in all sorts of incorrect ways.
That actually is an argument against Git: because Git doesn't model diffs, yet tries to simulate them, Git merges and rebases do all sort of incorrect things. For example, even the following simple example is not correctly handled in Git:
https://pijul.org/manual/why_pijul.html (see the figure "git merge vs pijul merge").
Which is why I disagree heavily with the example pijul merge you linked. Since lines AB are copied directly in one edit, and the other edit adds a line X after AB, why does pijul decide that the correct merge is only adding the X line after the second instance of AB?
Viewing this in terms of diffs instead of states means that everything is ambiguous rather than clear. What does it mean to do a diff on a diff? There are many definitions and few programmers will agree on what the proper algebraic operations are, and in fact the same person will often want different algebraic operations in different settings.
In the example above, if X is a necessary cleanup step after invoking AB (e.g a file close after file open and read), then pijul's interpretation of the ambiguous situation is wrong. But if X is a summary step that's necessary only once after the double invocation of AB, (e.g. sum += read(); n += 1;) and X is, for example, mean = sum / n, then only one X is needed.
Thinking about a "diff" without tying it to a full and complete starting state will always lead to these sorts of foundational problems, and it is quite clear from this pijul example that they haven't really thought about the problem much.
It doesn't decide, it only guarantees that the order between lines is preserved in all cases. Also, since you seem to have "thought a lot about the problem", you probably noticed that what Git does is totally wrong, since what it does depends on whether you merge both commits at once, or one at a time!
> the same person will often want different algebraic operations in different settings.
I doubt that when Alice adds line at the top of the file, while Bob edits the bottom of the file, anyone would ever want Bob's new lines merged in the middle of Alice's new lines.
Yet, that is what Git does.
> it is quite clear from this pijul example that they haven't really thought about the problem much.
Or maybe they have, and they have thought enough about it to notice that 3-way merge is doing the wrong thing, and that trying to merge snapshot doesn't even make any sense. They might have also thought enough about Git to understand that rebase is turning commits into patches, one by one, in order to "replay them on top of the current version".
While I do agree that full versions are important for storage (and all version control systems, including patch-based ones, are able to recover full versions), they are not very useful to merge work, or to solve conflicts.
When I git push, I upload my commits (i.e. snapshots). And when I git cherry-pick, I apply a diff. Both views are needed, I don't see why it's a leak.
Not completely: think of conflict resolutions for example. In patch-based systems such as Darcs and Pijul, you don't need a "rerere" command to handle that special case.
And I'm sure you'll agree that conflicts is the one critical situation where we need the best possible tool.
> When I git push, I upload my commits (i.e. snapshots). And when I git cherry-pick, I apply a diff
That's not how Git does it. Instead, Git runs 3-way merge, which doesn't do what you expect, see https://pijul.org/manual/why_pijul.html.
At a lower layer than that (the pack files) it does use binary diffs to store similar objects as deltas to save on disk space.
TL;DR diffs play an important part in git's machinery at different levels of abstraction. They just aren't part of how it represents its domain model.