Subject: [PATCH v2] Allow git mv FileA fILEa on case ignore file systems

From: Torsten Bögershausen <[email-masked]>
Date: Sat, 19 Mar 2011 15:28:33 +0100

The typical use case is when a file "FileA" should be renamed into fILEa
and we are on a case insenstive file system (system core.ignorecase = true).
Source and destination are the same file, it can be accessed under both names.
This makes git think that the destination file exists.
Unless used with --forced, git will refuse the "git mv FileA fILEa".
This change will allow "git mv FileA fILEa" under the following condition:
On Linux/Unix/Mac OS X the move is allowed when the inode of the source and
destination are equal (and they are on the same device).
This allows renames of MÄRCHEN into Märchen on Mac OS X.
(As a side effect, a file can be renamed to a name which is already
hard-linked to the same inode).
On Windows, the function win_is_same_file() from compat/win32/same-file.c
is used.
It calls GetFileInformationByHandle() to check if both files are
"the same".

Signed-off-by: Torsten Bögershausen <[email-masked]>
---
 Makefile                 |    8 +++++---
 builtin/mv.c             |    2 +-
 compat/win32/same-file.c |   26 ++++++++++++++++++++++++++
 git-compat-util.h        |   15 +++++++++++++++
 t/t7001-mv.sh            |   29 +++++++++++++++++++++++++++++
 5 files changed, 76 insertions(+), 4 deletions(-)
 create mode 100644 compat/win32/same-file.c

diff --git a/Makefile b/Makefile
index 5c2b797..55b9a05 100644
--- a/Makefile
+++ b/Makefile
@@ -924,7 +924,7 @@ ifeq ($(uname_O),Cygwin)
 	# Try commenting this out if you suspect MMAP is more efficient
 	NO_MMAP = YesPlease
 	X = .exe
-	COMPAT_OBJS += compat/cygwin.o
+	COMPAT_OBJS += compat/cygwin.o compat/win32/same-file.o
 	UNRELIABLE_FSTAT = UnfortunatelyYes
 endif
 ifeq ($(uname_S),FreeBSD)
@@ -1104,7 +1104,8 @@ ifeq ($(uname_S),Windows)
 	BASIC_CFLAGS = -nologo -I. -I../zlib -Icompat/vcbuild -Icompat/vcbuild/include -DWIN32 -D_CONSOLE -DHAVE_STRING_H -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE
 	COMPAT_OBJS = compat/msvc.o compat/winansi.o \
 		compat/win32/pthread.o compat/win32/syslog.o \
-		compat/win32/sys/poll.o compat/win32/dirent.o
+		compat/win32/sys/poll.o compat/win32/dirent.o \
+		compat/win32/same-file.o
 	COMPAT_CFLAGS = -D__USE_MINGW_ACCESS -DNOGDI -DHAVE_STRING_H -DHAVE_ALLOCA_H -Icompat -Icompat/regex -Icompat/win32 -DSTRIP_EXTENSION=\".exe\"
 	BASIC_LDFLAGS = -IGNORE:4217 -IGNORE:4049 -NOLOGO -SUBSYSTEM:CONSOLE -NODEFAULTLIB:MSVCRT.lib
 	EXTLIBS = user32.lib advapi32.lib shell32.lib wininet.lib ws2_32.lib
@@ -1177,7 +1178,8 @@ ifneq (,$(findstring MINGW,$(uname_S)))
 	COMPAT_CFLAGS += -DSTRIP_EXTENSION=\".exe\"
 	COMPAT_OBJS += compat/mingw.o compat/winansi.o \
 		compat/win32/pthread.o compat/win32/syslog.o \
-		compat/win32/sys/poll.o compat/win32/dirent.o
+		compat/win32/sys/poll.o compat/win32/dirent.o \
+		compat/win32/same-file.o
 	EXTLIBS += -lws2_32
 	PTHREAD_LIBS =
 	X = .exe
diff --git a/builtin/mv.c b/builtin/mv.c
index 93e8995..96792bd 100644
--- a/builtin/mv.c
+++ b/builtin/mv.c
@@ -166,7 +166,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
 			bad = "not under version control";
 		else if (lstat(dst, &st) == 0) {
 			bad = "destination exists";
-			if (force) {
+			if (force || is_same_file(src, dst)) {
 				/*
 				 * only files can overwrite each other:
 				 * check both source and destination
diff --git a/compat/win32/same-file.c b/compat/win32/same-file.c
new file mode 100644
index 0000000..bb1a791
--- /dev/null
+++ b/compat/win32/same-file.c
@@ -0,0 +1,26 @@
+#include "../../git-compat-util.h"
+#include "../win32.h"
+
+int win_is_same_file(const char *a, const char *b)
+{
+	BY_HANDLE_FILE_INFORMATION hia, hib;
+	HANDLE h;
+
+	h = CreateFile(a, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
+	if (INVALID_HANDLE_VALUE == h)
+		return 0;
+	if (!(GetFileInformationByHandle(h,&hia)))
+		return 0;
+  CloseHandle(h);
+
+	h = CreateFile(b, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
+	if (INVALID_HANDLE_VALUE == h)
+		return 0;
+	if (!(GetFileInformationByHandle(h,&hib)))
+		return 0;
+  CloseHandle(h);
+
+	return hia.dwVolumeSerialNumber == hib.dwVolumeSerialNumber &&
+	       hia.nFileSizeLow == hib.nFileSizeLow &&
+	       hia.nFileSizeHigh == hib.nFileSizeHigh;
+}
diff --git a/git-compat-util.h b/git-compat-util.h
index 49b50ee..df95458 100644
--- a/git-compat-util.h
+++ b/git-compat-util.h
@@ -152,6 +152,21 @@
 #include "compat/msvc.h"
 #endif
 
+#if defined (WIN32) || defined(__CYGWIN__)
+/* MinGW or MSVC or cygwin */
+int win_is_same_file(const char *a, const char *b);
+#define is_same_file(a,b) win_is_same_file((a),(b))
+#else
+static inline int is_same_file(const char *a, const char *b)
+{
+	struct stat sta, stb;
+	if (lstat(a, &sta) ||
+	    lstat(b, &stb))
+		return 0;
+	return sta.st_ino && sta.st_dev == stb.st_dev && sta.st_ino == stb.st_ino;
+}
+#endif
+
 #ifndef NO_LIBGEN_H
 #include <libgen.h>
 #else
diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh
index a845b15..d0e73ee 100755
--- a/t/t7001-mv.sh
+++ b/t/t7001-mv.sh
@@ -255,4 +255,33 @@ test_expect_success SYMLINKS 'git mv should overwrite file with a symlink' '
 
 rm -f moved symlink
 
+touch x
+if ln x y 2>/dev/null; then
+	hardlinks=1
+fi
+rm -f x y
+
+if test "$(git config --bool core.ignorecase)" = true -o "$hardlinks"; then
+	test_expect_success 'git mv FileA fILEa' '
+
+		rm -fr .git * &&
+		git init &&
+		echo FileA > FileA &&
+		git add FileA &&
+		git commit -m add FileA &&
+		{
+			if ! test -f fILEa; then
+				ln FileA fILEa
+			fi
+		} &&
+		git mv FileA fILEa &&
+		git commit -m "mv FileA fILEa" &&
+		rm -f FileA fILEa &&
+		git reset --hard &&
+		test "$(echo *)" = fILEa
+	'
+else
+	say "Neither ignorecase nor hardlinks, skipping git mv FileA fILEa"
+fi
+
 test_done
-- 
1.7.4