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?
CodePudding user response:
First, let's clear-up some misconceptions...
- A
git
commit is a snapshot of your entire repo at a given point-in-time. - A
git
commit is not a diff or changeset. - A
git
commit does not contain any file "rename" information. - And
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.
So how does git
handle moved or renamed files?
...but my 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 yourHEAD
commit (aka "Compare with Unmodified").For example:
- Consider commit "snapshot 1" with 2 files:
Foo.txt
andBar.txt
. - Then you rename
Foo.txt
toQux.txt
(and make no other changes). - Then save that as a new commit ("snapshot 2").
- If you ask
git
todiff
"snapshot 1" with "snapshot 2" then git can see thatFoo.txt
was renamed toQux.txt
(andBar.txt
was unchanged) because the contents (and consequently the files' cryptographic hashes) are identical, therefore it infers that a file rename fromFoo.txt
toQux.txt
occurred.- Fun-fact: if you ask
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 fromQux.txt
back toFoo.txt
.
- Fun-fact: if you ask
- Consider commit "snapshot 1" with 2 files:
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.
- This is not a bug, but a feature: this behaviour means that
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. - For example, consider a refactoring scenario where you would split a
MultipleClasses.cs
file containing multipleclass
definitions into separate.cs
files, with oneclass
per file. In this case there is no real "rename" being performed andgit
's diff would show you 1 file being deleted (MultipleClassesw.cs
) at the same time as the newSingleClass1.cs
,SingleClass2.cs
, etc files are added.- I imagine that you wouldn't want it to be saved to source-control history as a rename from
MultipleClasses.cs
toSingleClass1.cs
as it would in SVN or TFS if you allowed the first rename to be saved as a rename in SVN/TFS.
- I imagine that you wouldn't want it to be saved to source-control history as a rename from
- This is not a bug, but a feature: this behaviour means that
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.- (This does mean that I do need to temporarily rename files back when using VS' Refactor Rename functionality, fwiw, so I can make a commit with edited files but without any renamed files).
What does any of this have to do with Visual Studio though?
Consider this scenario:
- You have an existing git repo for a C# project with no pending changes (staged or otherwise). The project has a file located at
Project/Foobar.cs
containingclass Foobar
. The file is only about 1KB in size. - You then use Visual Studio's Refactor > Rename... feature to rename a
class Foobar
toclass Barfoo
.- Visual Studio will not-only rename
class Foobar
toclass Barfoo
and edit all occurrences ofFoobar
elsewhere in the project, but it will also renameFoobar.cs
toBarfoo.cs
. - In this example, the identifier
Foobar
only appears in the 1KB-sizedFoobar.cs
file two times (first inclass Foobar
, then again in the constructor definitionFoobar() {}
) 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-ingit
GUI) only sees the last commit withFoobar.cs
, and sees the current HEAD (with the uncommitted changes) hasBarfoo.cs
which is 1% different fromFoobar.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.- However, if you make more substantial changes to
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.- And if you manually revert some of the changes to
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.
- And if you manually revert some of the changes to
- Visual Studio will not-only rename
- You have an existing git repo for a C# project with no pending changes (staged or otherwise). The project has a file located at
A neat thing about
git
not storing actual file renames/moves in commits is that it means that you can safely usegit
with any software, including any software that renames/moves files! Especially software that is not source-control aware.- Previously, with SVN and TFS, you needed to restrict yourself to software programs that had built-in support for whatever source-control system you were using (and handled renames itself) or software that supported MSSCCI (and so saved renames via MSSCCI), otherwise you had to use a separate SVN or TFS client to save/commit your file-renames (e.g. TortoiseSvn and Team Foundation Explorer, respectively). This was a tedious and error-prone process that I'm glad to see the end of.
Consequently, there is no need for Visual Studio (with or without
git
support baked-in) to informgit
that a file was renamed/moved.- That's why there's no IDE support for it: because it simply isn't needed.
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.
- (After-all, how can you meaningfully reorder a file rename operation?)