31

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?

13
  • 2
    Fun fact: git does not have a concept of "move" or "rename". Remember: a git commit is a snapshot, not a diff/delta. Commented Dec 18, 2021 at 7:32
  • Does this answer your question? Handling file renames in Git Commented Dec 18, 2021 at 7:32
  • 1
    @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 question Commented Dec 18, 2021 at 7:35
  • 2
    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. Commented Dec 18, 2021 at 8:25
  • stackoverflow.com/search?q=%5Bgit%5D+rename+detection Commented Dec 18, 2021 at 12:39

3 Answers 3

30

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?)
Sign up to request clarification or add additional context in comments.

3 Comments

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.
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.)
Still unbelievable!! (Include me as well - my mind is blown in a mind blowing manner!!). That took me long trying to DEBUG/Troubleshoot something that was inherently a FEATURE!! :)
6

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.

1 Comment

Thank you, Lauri! Note that this is an extension for VSCode, not Visual Studio.
4

If you look at Dai's answer, basically git mv seems to just be a wrapper for deleting the old name, adding the new name, and staging the changes. So, if you want this to work in Visual Studio, make all the rename changes you want, select all those changes, then right-click and Stage changes.

Then it will show up in the Staged Changes list as a rename.

3 Comments

I'm not sure if this is fully true. Git does try to find the renamed/moved files during staging by checking their hash and the content matching (the similarity percentage can be adjusted) but this process is not deterministic. Moreover the content matching process is slow.
I guess I don’t know if the why is correct, but since finding this question the process has been working for me. I added this answer because the one above was very complex and I thought it could use an answer that straightforwardly describes what to do.
You answer is not wrong, but the thing is that there are no perfect answer/solution to this problem at the moment. Feel free to check this post and the comments below. and also this comment.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.