Context
I often move, rename files in Visual Studio 2022. Rename is a standard refactoring practice. However when I rename a file in Solution Explorer, not git mv
operation is performed, instead git delete and git add.
This causes loosing the history of that particular file/class, which is a great loss in many cases.
Question
I can do the move operation leaving the IDE and using command line
git mv myoldfile.cs mynewfile.cs
which will keep history perfectly, but leaving the IDE is a productivity killer, especially when talking about refactoring and renaming multiple classes/files.
How to perform git mv
within Visual Studio, instead of git delete and git add, when renaming, moving files in Solution Explorer?
git
commit is a snapshot of your entire repo at a given point-in-time.git
commit is not a diff or changeset.git
commit does not contain any file "rename" information.git
itself does not log, monitor, record, or otherwise concern itself with files that are moved or renamed (...at the point of creating a commit).The above might be counter-intuitive, or even mind-blowing for some people (myself included, when I first learned this) because it's contrary to all major preceding source-control systems like SVN, TFS, CSV, Perforce (Prior to Helix) and others, because all of those systems do store diffs or changesets and it's fundamental to their models.
Internally, git
does use various forms of diffing and delta-compression, however those are intentionally hidden from the user as they're considered an implementation detail. This is because git's domain model is entirely built on the concept of atomic commits, which represent a snapshot state of the entire repo at a particular point-in-time. Also, uses your OS's low-level file-change-detection features to detect which specific files have been changed without needing to re-scan your entire working directory: on Linux/POSIX it uses lstat
, on Windows (where lstat
isn't available) it uses fscache
. When git computes hashes of your repo it uses Merkel Tree structures to avoid having to constantly recompute the hash of every file in the repo.
git
handle moved or renamed files?git
GUI clearly shows a file rename, not a file delete+add or edit!While git
doesn't store information about file renames, it still is capable of heuristically detecting renamed files between any two git commits, as well as detecting files renamed/moved between your un-committed repo's working directory tree and your HEAD
commit (aka "Compare with Unmodified").
For example:
Foo.txt
and Bar.txt
.Foo.txt
to Qux.txt
(and make no other changes).git
to diff
"snapshot 1" with "snapshot 2" then git can see that Foo.txt
was renamed to Qux.txt
(and Bar.txt
was unchanged) because the contents (and consequently the files' cryptographic hashes) are identical, therefore it infers that a file rename from Foo.txt
to Qux.txt
occurred.
git
to do the same diff, but use "snapshot 2" as the base commit and "snapshot 1" as the subsequent commit then git will show you that it detected a rename from Qux.txt
back to Foo.txt
.However, if you do more than just rename or move a file between two commits, such as editing the file at the same time, then git may-or-may-not consider the file a new separate file instead of a renamed file.
git
can handle common file-system-level refactoring operations (like splitting files up) far better than file-centric source-control (like TFS and SVN) can, and you won't see refactor-related false renames either.MultipleClasses.cs
file containing multiple class
definitions into separate .cs
files, with one class
per file. In this case there is no real "rename" being performed and git
's diff would show you 1 file being deleted (MultipleClassesw.cs
) at the same time as the new SingleClass1.cs
, SingleClass2.cs
, etc files are added.
MultipleClasses.cs
to SingleClass1.cs
as it would in SVN or TFS if you allowed the first rename to be saved as a rename in SVN/TFS.But, and as you can imagine, sometimes git
's heuristics don't work and you need to prod it with --follow
and/or --find-renames=<percentage>
(aka -M<percentage>
).
My personal preferred practice is to keep your filesystem-based and edit-code-files changes in separate git commits (so a commit contains only edited files, or only added+deleted files, or only split-up changes), that way you make it much, much easier for git's --follow
heuristic to detect renames/moves.
Consider this scenario:
Project/Foobar.cs
containing class Foobar
. The file is only about 1KB in size.class Foobar
to class Barfoo
.
class Foobar
to class Barfoo
and edit all occurrences of Foobar
elsewhere in the project, but it will also rename Foobar.cs
to Barfoo.cs
.Foobar
only appears in the 1KB-sized Foobar.cs
file two times (first in class Foobar
, then again in the constructor definition Foobar() {}
) so only 12 bytes (2 * 6 chars) are changed. In a 1KB file that's a 1% change (12 / 1024 == 0.0117 --> 1.17%
).git
(and Visual Studio's built-in git
GUI) only sees the last commit with Foobar.cs
, and sees the current HEAD (with the uncommitted changes) has Barfoo.cs
which is 1% different from Foobar.cs
so it considers that a rename/move instead of a Delete+Add or an Edit, so Visual Studio's Solution Explorer will use the "Move/Rename" git status icon next to that file instead of the "File edited" or "New file" status icon.Barfoo.cs
(without committing yet) that exceed the default change % threshold of 50% then the Solution Explorer will start showing the "New file" icon instead of "Renamed/moved file" icon.
Barfoo.cs
(again: without saving any commits yet) such that it slips below the 50% change threshold then VS's Solution Explorer will show the Rename icon again.A neat thing about git
not storing actual file renames/moves in commits is that it means that you can safely use git
with any software, including any software that renames/moves files! Especially software that is not source-control aware.
Consequently, there is no need for Visual Studio (with or without git
support baked-in) to inform git
that a file was renamed/moved.
The fact that a git commit isn't a delta, but a snapshot, means you can far more easily reorder commits, and rebase entire branches with minimal pain. This is not something that was really possible at all in SVN or TFS.