Search code examples
gitgit-diff

Two versions of same line in "git diff"


I've encountered strange behavior after switching branches. I've executed git checkout master, and I immediately had one file marked as modified. I ran git reset --hard to get rid of it, but to no effect. git diff gave me output like this:

diff --git a/Project/MYClass.cs b/Project/MYClass.cs
index 4f8405e..cb8ca4c 100644
--- a/Project/MYClass.cs
+++ b/Project/MYClass.cs
@@ -170,7 +170,7 @@ namespace Project
-            Logger.Warn(message);
+            Logger.Info(message);

But when I manually change it back, I get:

diff --git a/Project/MyClass.cs b/Project/MyClass.cs
index cb8ca4c..4f8405e 100644
--- a/Project/MyClass.cs
+++ b/Project/MyClass.cs
@@ -170,7 +170,7 @@ namespace Project
-            Logger.Info(message);
+            Logger.Warn(message);

So git can't decide which one was it. To make it funnier, if I change it to something completely different, I get:

diff --git a/Project/MYClass.cs b/Project/MYClass.cs
index 4f8405e..cb8ca4c 100644
--- a/Project/MYClass.cs
+++ b/Project/MYClass.cs
@@ -170,7 +170,7 @@ namespace Project
-            Logger.Warn(message);
+            Logger.Error(message);
diff --git a/Project/MyClass.cs b/Project/MyClass.cs
index cb8ca4c..4f8405e 100644
--- a/Project/MyClass.cs
+++ b/Project/MyClass.cs
@@ -170,7 +170,7 @@ namespace Project
-            Logger.Info(message);
+            Logger.Error(message);

When I ran git ls-tree -r master | Select-String "myclass.cs" I got this:

100644 blob 4f8405e28faa09981a3f76775b1693db31b0bdad    Project/MYClass.cs
100644 blob cb8ca4c60f10f959422ecc2cbdb91d0e693ea380    Project/MyClass.cs

I see and understand something is off, but I have no idea how did it happen.

I know how to fix it brutal way, removing my local and fetching everything from remote, but I want to know what causes it, and how to fix it properly.


Solution

  • Git believes you have two different files in the Project directory / folder:

    • MYClass.cs (uppercase M, uppercase Y, uppercase C, lowercase lass.cs)
    • MyClass.cs (uppercase M, lowercase y, uppercase C, lowercase lass.cs)

    Git opens each file name separately, writes out the contents for that file, and closes it.

    Since you are on Windows, which refuses to make two different files whose names differ only in case, your OS overwrites one of the two files with the other one, leaving only one of the two casings in place. Your OS won't let Git have two files in the work-tree. Git can (and does) have both files in the index, because Git's index is actually a data file, not a directory / folder. Since Git makes its commits out of the index, Git can make new commits that continue to contain two different files whose name differs only in case.

    You must update your index so that it contains only one such name, otherwise you're doomed on Windows (and MacOS) to forever fight this problem. The easy way is to do the fixing on a Linux or Unix system, where the index and work-tree stay in sync since the OS can and does create two different files whose name differs only in case.

    The hard way is to use git rm --cached: give it the one name you really want gone. Git will remove that file from the index, without affecting the work-tree at all. Now that the index has the correct case, you can fix the case in the work-tree to match, however one does this on Windows—on other systems, the usual trick is:

    mv NameWithCASEISSUE x   # change the whole name a lot, using a safe name
    mv x NameWithCaseIssue   # change it back, using the correct case this time
    

    Now you can compare the index version with the work-tree version and make any adjustments that might be required. Since there's only one index version that will match the work-tree file, regardless of case issues (of which there aren't any now, because you have carefully made the filename cases match), you can get your job done.

    The really unfortunate case (if I can use that word :-) ) occurs when you really do want both file names, differing only in case, in the index and in future commits. There are no good tools for dealing with this. It's theoretically possible to build such tools around the git update-index and git checkout-index commands, but they do not exist at this time.