commit 0123456789abcdef0123456789abcdef01234567
parent fedcba9876543210fedcba9876543210fedcba98
author Nemo <[email protected]> 1234567890 +0000
committer Nemo <[email protected]> 1234567890 +0000
rename-from path1.old
rename-to path1.new
rename-from path2.old
rename-to path2.new
Commit message
And you don’t detect moves (because that’s madness), but require that people record them deliberately, just like every other VCS has done. There’s even git-mv already, it just skips a step that every other VCS’s equivalent command would do. (And technically this all works out because the index is a commit, so you can record the rename normally.)Of course, all of this assumes that moving a file is a meaningful operation. Perhaps ideally (for most languages and systems) you’d track this in far smaller chunks, so that you can track changes to a function even when it alone was moved to a different file. But things like Git aren’t interested in those kinds of semantics, and work technically at the file level, more or less, so I think it should track renames because in practice straightforward renames are super common, but often also involve other changes that thwart rename detection. Years ago Linus explained why he didn’t like storing moves (someone else has linked it), but I’m largely not sold with his reasoning—the theory of the perfect has hindered the useful, and file renames are commonly meaningful in ways more than he said.
(yes, some IDE might have git integration, but personally I don't like my IDE messing with git, except read-only (annotate, diff))
Like I implicitly said: how to do it beyond the “simple identical content cases”?
But if the solution is for the user to explicitly order renames (i.e., this renamed Java class is a file move) then the solution is indeed simple.
I see the point that Linus was making that you may want to be able to see “function moves” and so on. But in practice I am very often interested in file moves since you can inspect the file history easily in Git—except when you hit some wall because someone renamed the file. Then you need to re-run the command with `--follow`. Contrast all of that with a function move... I almost never can summon the will to fish out the incantation (like a regex or a robust line range) which will give me the history of a function across intra- or inter-file moves and so on.
is there an intuitive and enlightening explanation as to why it is this way?
As to why Linus doesn’t like storing file moves: https://public-inbox.org/git/Pine.LNX.4.58.0504150753440.721...
A few counter points:
- `hg` has `cp`, and I believe both Meta and Google's internal systems have that; - git has `mv`, which was added later, but it is really janky and git would forget files are moved which I think it is because git doesn't try to track that, likely because of the philosophy here; - as for storing file moves - nobody said you *have* to use this information, but you can certainly use this information to help with things.
The whole thread is an interesting read though and I will try going through it someday - maybe doing that would change my mind.
Having some sort of notion of "parent branch" would be very useful for a number of common operations, and a "renamed file" without having to rely on client dependent heuristics too. Empty files trip people up all the time so a "create file" would fit in perfectly.
These concepts would also be a good basis for more user friendly clients. Other version control systems do this the surprise factor should be low.
In short, Linus stance is that file renaming doesn’t matter, only the contents of files matter, and the moving of contents between files. Moved/renamed files then fall out as a special case of moving content.
Personally, I think this is a case of the better being the enemy of the good, and his “clearly superior algorithm” doesn’t work as well as claimed in practice. Or maybe tooling merely still isn’t up to snuff after 18 years.
Many people develop a bad mental model with commits as diffs, because that's what the UI makes them think commits are. It can work for a while, but inevitably leads to confusion later on.
Viewing the commit graph in terms of nodes (commits) or edges (diffs) is equivalent, these are dual views you can easily convert between. The internal representation is independent from that. Some VCSs use a mix of diffs and full revisions internally. Even Git uses delta compression when packing objects.
It stores a commit graph, and a tree at each of those commits. (A lossless compression algorithm deduplicates information.)
There's no need for the author to be concerned with what diffing information gets incorporated into the commit. Diffs are up to the viewer of the commit history.
git show --diff-algorithm=...
One example that also bites people: moving files isn't stored in git - if you move files (even with `git mv`) and create a new commit, the moves aren't stored, but this is reconstructed later by the client based on similarity, which comes from the diff algorithm.
And git has multiple diff algorithms to pick from: https://git-scm.com/docs/git-config#Documentation/git-config...
And optionally to not detect renames in diff output with `diff.renames`: https://git-scm.com/docs/git-config#Documentation/git-config...