How to perform git move in Visual Studio, instead of git delete and git add, when renaming, moving files in Solution Explorer?
Asked Answered
R

2

25

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?

Reify answered 18/12, 2021 at 7:30 Comment(13)
Fun fact: git does not have a concept of "move" or "rename". Remember: a git commit is a snapshot, not a diff/delta.Bolshevist
Does this answer your question? Handling file renames in GitBolshevist
@dai, maybe it is not clear in my question, I do know how to move files with git, (git mv) I am asking how to do this not leaving the IDE, and issue a git mv oldname.cs newname.cs I am going to edit the questionReify
You do know that git mv doesn't store anything "special" or unique in your repo? A git mv is identical to physically moving files yourself or by any other tool. That's why there's no IDE support for it: because it simply isn't needed.Bolshevist
stackoverflow.com/search?q=%5Bgit%5D+rename+detectionHeteropterous
@Heteropterous many thx, that is useful. I have to rephrase my question about, why git rename detection is not working, if I move the files within the IDE. Clearly, if I use the Solution Explorer to rename the file, I ended with a separate delete and add with loosing history, if I do that outside the ide, the rename detection works perfectly, and the new file has the complete historyReify
@dai, thx, but you can not make statements about the IDE without acually experiencing it...Reify
@dai: I was talking experience about the current and particular behaviour, not the skill. Please try it, there is a difference between move/rename using the solution explorer, and move/rename with git mv command. In case this statement is true, then your statement "That's why there's no IDE support for it: because it simply isn't needed." is false.Reify
@Reify Visual Studio will detect it as a Move only if its own heuristics say so (which are separate from git's built-in move-detection logic). And there are plenty of operations in VS that trigger a filesystem move-or-rename that the Solution Explorer will show as delete-and-new and plenty that make Solution Explorer show as an actual move. If you encounter differences, blame the heuristics (which can change between individual VS releases).Bolshevist
@Reify If you're using VS's "Refactor > Rename" then that will almost certainly be detected as a delete+new instead of a rename because there's a threshold limit (for changes to a moved/renamed file) that VS (and git) uses to determine if a file was renamed or if it thinks it's a brand new file - using Refactor Rename will most likely pass that threshold and cause it to consider it a delete+new instead of a renamed file.Bolshevist
that makes sense. I will conduct a more isolated experiment.Reify
@Dai, your two comments about git move detection and the last one about refactor changes are the answer. In case you are going to post as answer I am going to accept it, otherwise probably I should delete this questionReify
@Reify I've written-up my thoughts as an answer and posted it now. I'd appreciate your feedback.Bolshevist
B
26

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 your HEAD commit (aka "Compare with Unmodified").

  • For example:

    • Consider commit "snapshot 1" with 2 files: Foo.txt and Bar.txt.
    • Then you rename Foo.txt to Qux.txt (and make no other changes).
    • Then save that as a new commit ("snapshot 2").
    • If you ask 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.
      • 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 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.

    • 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 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.
      • I imagine that you wouldn't want it to be saved to source-control history as a rename from 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.

    • (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 containing class Foobar. The file is only about 1KB in size.
    • You then use Visual Studio's Refactor > Rename... feature to rename a class Foobar to class Barfoo.
      • Visual Studio will not-only rename 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.
      • In this example, the identifier 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.
      • 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.
  • 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.

    • 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 inform git 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?)
Bolshevist answered 22/12, 2021 at 5:54 Comment(2)
It's a bit confusing because if you rename it and do a git status it does not tell you it's been moved like git mv does. But when you commit it, it does.Extrusive
That's because git mv also stages the old and new file. If you do the rename manually and then you do git add on both the old and the new file(name) so the change gets staged, you will see it as rename, even before committing. (The opposite is also true: If you do git mv but then you do git reset to unstage the changes, you will see an unstaged delete+add afterwards.)Fionnula
A
0

There's a plugin that does this https://marketplace.visualstudio.com/items?itemName=ambooth.git-rename

I can't fathom why the accepted answer is a long winded explanation of "why things are this way" when they could be (and are) better (less hassle) in different IDEs.

Atelectasis answered 22/7 at 16:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.