{"thread":{"id":"27047","subject":"[PATCH] Allow git mv FILENAME Filename when core.ignorecase = true","startedAt":"2011-04-10T05:50:29Z","lastAt":"2011-04-14T05:39:26Z","messageCount":2,"participants":["Torsten Bögershausen","Joshua Juran"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"165530","messageId":"201104100750.29950.tboegi@web.de","threadId":"27047","inReplyTo":null,"subject":"[PATCH] Allow git mv FILENAME Filename when core.ignorecase = true","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2011-04-10T05:50:29Z","receivedAt":"2011-04-10T05:50:29Z","isPatch":true,"sender":{"key":"tboegi@web.de","avatar":"https://avatars.githubusercontent.com/u/7138363?v=4"},"body":"Motivation:\nThe typical use case is when a file named \"FILENAME\" should be\nrenamed into \"Filename\" and we are on a case ignoring file system\n(core.ignorecase = true).\n\nUsing \"mv FILENAME Filename\" outside git succeeds,\n(on Windows and MAC OS X, under Linux the mv command rejects\n\"mv: `Filename' and `FILENAME' are the same file\").\n\n\"git mv FILENAME Filename\" is refused, \"fatal: destination exists\",\nunless \"git mv --forced FILENAME Filename\" is used.\nThe underlying file system makes git think that the\ndestination \"Filename\" exists.\n\nThe following discussion assumes, that we are on a\n\"case ignoring\" file system, and core.ignorecase = true.\n\nThis change allows \"git mv FILENAME Filename\".\nUsing non ASCII works as well, like \"git mv MÄRCHEN Märchen\".\nThe ambition is that \"git mv FILENAME Filename\" changes both\nthe git index and the filename in the working tree,\nin the same way how \"git mv Filename NewFile\" works.\nNote: Under Linux+vfat The rename() function does not the rename\nin the working directory.\n\nImplementation details:\nA possible approach to allow the \"git mv FILENAME Filename\"\nis to compare both file names using strcasecmp().\n\nThis works for filenames where all characters are ASCII,\nIt will fail for \"git mv MÄRCHEN Märchen\".\n\nGit has now idea about the encoding of filenames\n(like UTF-8, ISO-8859-1 or any other).\nNeither has strcasecmp() an idea how to handle non ASCII characters.\n\nWith this patch git lets the underlying file system decide\nif 2 file names refer to the same file.\n\nRemember that the file system does this already, by returning the\nsame values for lstat(\"FILENAME\") and lstat(\"Filename\").\n\nBy comparing all members in \"struct stat\" we can be sure that\nboth filenames point out the same file.\nThis is done in the function \"equivalent_filenames()\".\n\nAs lstat() on Windows (mingw.c or cygwin.c) sets st_ino to 0,\n(and st_dev and other fields in struct stat)\nwe need other checks when running under Windows.\n\nTherefore a different implementation of equivalent_filenames() is used\nunder Windows.\nIt uses GetFileInformationByHandle() to get and compare\ndwVolumeSerialNumber, nFileIndexLow and nFileIndexHigh.\nIt uses even lstat(), since Windows reports the same nFileIndexLow/High for\na file and a softlink (under cygwin) pointing to it.\n\nTo summarize:\nequivalent_filenames() is OS specific and checks under Windows:\ndwVolumeSerialNumber, nFileIndexLow/High, st_mode, st_size,\nst_atime and st_mtime.\nAll other OS check\nst_mode, st_dev, st_ino, st_uid, st_gid, st_size, st_atime, st_mtime.\n\nAs a bonus (or regression), a file name can be renamed to a file name\nwhich is already hard-linked to the same inode.\n\nSigned-off-by: Torsten Bögershausen <tboegi@web.de>\n---\n builtin/mv.c      |   10 +++++++-\n compat/cygwin.c   |   44 ++++++++++++++++++++++++++++++++++++++++\n compat/cygwin.h   |    3 ++\n compat/mingw.c    |   33 ++++++++++++++++++++++++++++++\n compat/mingw.h    |    3 ++\n git-compat-util.h |   17 +++++++++++++++\n t/t7001-mv.sh     |   57 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n t/test-lib.sh     |    4 +++\n 8 files changed, 169 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 40f33ca..7be7d8a 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -165,14 +165,20 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t} else if (cache_name_pos(src, length) < 0)\n \t\t\tbad = _(\"not under version control\");\n \t\telse if (lstat(dst, &st) == 0) {\n+\t\t\tint allow_force = force;\n \t\t\tbad = _(\"destination exists\");\n-\t\t\tif (force) {\n+\t\t\tif (!force && ignore_case && equivalent_filenames(src, dst)) {\n+\t\t\t\tallow_force = 1;\n+\t\t\t\tbad = NULL;\n+\t\t\t}\n+\t\t\tif (allow_force) {\n \t\t\t\t/*\n \t\t\t\t * only files can overwrite each other:\n \t\t\t\t * check both source and destination\n \t\t\t\t */\n \t\t\t\tif (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {\n-\t\t\t\t\twarning(_(\"%s; will overwrite!\"), bad);\n+\t\t\t\t\tif (bad)\n+\t\t\t\t\t\twarning(_(\"%s; will overwrite!\"), bad);\n \t\t\t\t\tbad = NULL;\n \t\t\t\t} else\n \t\t\t\t\tbad = _(\"Cannot overwrite\");\ndiff --git a/compat/cygwin.c b/compat/cygwin.c\nindex b4a51b9..4fdd94a 100644\n--- a/compat/cygwin.c\n+++ b/compat/cygwin.c\n@@ -1,6 +1,7 @@\n #define WIN32_LEAN_AND_MEAN\n #include \"../git-compat-util.h\"\n #include \"win32.h\"\n+#include <io.h>\n #include \"../cache.h\" /* to read configuration */\n \n static inline void filetime_to_timespec(const FILETIME *ft, struct timespec *ts)\n@@ -85,6 +86,49 @@ static int cygwin_stat(const char *path, struct stat *buf)\n \treturn do_stat(path, buf, stat);\n }\n \n+int cygwin_equivalent_filenames(const char *a, const char *b)\n+{\n+\tint fd;\n+\tBY_HANDLE_FILE_INFORMATION hia, hib;\n+\tHANDLE h;\n+\tstruct stat st_a, st_b;\n+\n+\tif (lstat(a, &st_a) || lstat(b, &st_b))\n+\t\treturn 0;\n+\n+\tfd = open(a, O_RDONLY);\n+\tif (-1 == fd)\n+\t\treturn 0;\n+\n+\th = (HANDLE)get_osfhandle(fd);\n+\tif (INVALID_HANDLE_VALUE == h)\n+\t\treturn 0;\n+\n+\tif (!(GetFileInformationByHandle(h,&hia)))\n+\t\treturn 0;\n+\tCloseHandle(h);\n+\tclose(fd);\n+\n+\tfd = open(b, O_RDONLY);\n+\tif (-1 == fd)\n+\t\treturn 0;\n+\n+\th = (HANDLE)get_osfhandle(fd);\n+\tif (INVALID_HANDLE_VALUE == h)\n+\t\treturn 0;\n+\tif (!(GetFileInformationByHandle(h,&hib)))\n+\t\treturn 0;\n+\tCloseHandle(h);\n+\tclose(fd);\n+\n+\treturn st_a.st_mode == st_b.st_mode &&\n+\t       st_a.st_size == st_b.st_size &&\n+\t       st_a.st_atime == st_b.st_atime &&\n+\t       st_a.st_mtime == st_b.st_mtime &&\n+\t       hia.dwVolumeSerialNumber == hib.dwVolumeSerialNumber &&\n+\t       hia.nFileIndexLow == hib.nFileIndexLow &&\n+\t       hia.nFileIndexHigh == hib.nFileIndexHigh;\n+}\n \n /*\n  * At start up, we are trying to determine whether Win32 API or cygwin stat\ndiff --git a/compat/cygwin.h b/compat/cygwin.h\nindex a3229f5..04cc17e 100644\n--- a/compat/cygwin.h\n+++ b/compat/cygwin.h\n@@ -7,3 +7,6 @@ extern stat_fn_t cygwin_lstat_fn;\n \n #define stat(path, buf) (*cygwin_stat_fn)(path, buf)\n #define lstat(path, buf) (*cygwin_lstat_fn)(path, buf)\n+\n+int cygwin_equivalent_filenames(const char *a, const char *b);\n+#define equivalent_filenames cygwin_equivalent_filenames\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 878b1de..56be81a 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -474,6 +474,39 @@ int mingw_fstat(int fd, struct stat *buf)\n \treturn -1;\n }\n \n+int mingw_equivalent_filenames(const char *a, const char *b)\n+{\n+\tBY_HANDLE_FILE_INFORMATION hia, hib;\n+\tHANDLE h;\n+\tstruct stat st_a, st_b;\n+\n+\tif (lstat(a, &st_a) || lstat(b, &st_b))\n+\t\treturn 0;\n+\n+\th = CreateFile(a, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);\n+\tif (INVALID_HANDLE_VALUE == h)\n+\t\treturn 0;\n+\n+\tif (!(GetFileInformationByHandle(h,&hia)))\n+\t\treturn 0;\n+\tCloseHandle(h);\n+\n+\th = CreateFile(b, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);\n+\tif (INVALID_HANDLE_VALUE == h)\n+\t\treturn 0;\n+\tif (!(GetFileInformationByHandle(h,&hib)))\n+\t\treturn 0;\n+\tCloseHandle(h);\n+\n+\treturn st_a.st_mode == st_b.st_mode &&\n+\t       st_a.st_size == st_b.st_size &&\n+\t       st_a.st_atime == st_b.st_atime &&\n+\t       st_a.st_mtime == st_b.st_mtime &&\n+\t       hia.dwVolumeSerialNumber == hib.dwVolumeSerialNumber &&\n+\t       hia.nFileIndexLow == hib.nFileIndexLow &&\n+\t       hia.nFileIndexHigh == hib.nFileIndexHigh;\n+}\n+\n static inline void time_t_to_filetime(time_t t, FILETIME *ft)\n {\n \tlong long winTime = t * 10000000LL + 116444736000000000LL;\ndiff --git a/compat/mingw.h b/compat/mingw.h\nindex 62eccd3..3445104 100644\n--- a/compat/mingw.h\n+++ b/compat/mingw.h\n@@ -303,6 +303,9 @@ int winansi_fprintf(FILE *stream, const char *format, ...) __attribute__((format\n void mingw_open_html(const char *path);\n #define open_html mingw_open_html\n \n+int mingw_equivalent_filenames(const char *a, const char *b);\n+#define equivalent_filenames mingw_equivalent_filenames\n+\n /*\n  * helpers\n  */\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex 40498b3..d66cffe 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -567,4 +567,21 @@ int rmdir_or_warn(const char *path);\n  */\n int remove_or_warn(unsigned int mode, const char *path);\n \n+#ifndef equivalent_filenames\n+static inline int equivalent_filenames(const char *a, const char *b) {\n+\tstruct stat st_a, st_b;\n+\tif (lstat(a, &st_a) || lstat(b, &st_b))\n+\t\treturn 0;\n+\n+\treturn st_a.st_mode == st_b.st_mode &&\n+\t       st_a.st_dev == st_b.st_dev &&\n+\t       st_a.st_ino == st_b.st_ino &&\n+\t       st_a.st_uid == st_b.st_uid &&\n+\t       st_a.st_gid == st_b.st_gid &&\n+\t       st_a.st_size == st_b.st_size &&\n+\t       st_a.st_atime == st_b.st_atime &&\n+\t       st_a.st_mtime == st_b.st_mtime;\n+}\n+#endif\n+\n #endif\ndiff --git a/t/t7001-mv.sh b/t/t7001-mv.sh\nindex a845b15..0c4b96a 100755\n--- a/t/t7001-mv.sh\n+++ b/t/t7001-mv.sh\n@@ -255,4 +255,61 @@ test_expect_success SYMLINKS 'git mv should overwrite file with a symlink' '\n \n rm -f moved symlink\n \n+unset encoding\n+ae_upper_asc=AE\n+ae_lower_asc=ae\n+ae_upper_utf8=$(printf '\\303\\206')\n+ae_lower_utf8=$(printf '\\303\\246')\n+\n+for enc in utf8 asc ; do\n+\teval ae_lower=\\$ae_lower_$enc\n+\teval ae_upper=\\$ae_upper_$enc\n+\tif (>./$ae_lower && echo broken > ./$ae_upper && test x\"$(cat $ae_lower)\" = xbroken ) 2>/dev/null ; then\n+\t\tif err=$(mv $ae_lower $ae_upper 2>&1); then\n+\t\t\tunset err\n+\t\t\tencoding=$enc\n+\t\t\tbreak\n+\t\tfi\n+\telse\n+\t\terr=\"case sensitive file system\"\n+\tfi\n+done\n+\n+if test -n \"$encoding\"; then\n+\ttest_expect_success \"git mv AE ae $encoding\" '\n+\t\trm -fr .git * &&\n+\t\tgit init &&\n+\t\techo $encoding > $ae_upper &&\n+\t\tgit add $ae_upper &&\n+\t\tgit commit -m \"add AE\" &&\n+\t\tgit mv $ae_upper $ae_lower &&\n+\t\tgit commit -m \"mv AE ae\" &&\n+\t\trm -f $ae_upper $ae_lower &&\n+\t\tgit reset --hard &&\n+\t\ttest \"$(echo *)\" = $ae_lower\n+\t'\n+else\n+\tsay \"Skipping 'git mv AE ae' $err ($enc)\"\n+fi\n+\n+test_expect_success HARDLINKS 'git mv FILE File HARDLINKED' '\n+\trm -fr .git * &&\n+\tgit init &&\n+\tgit config core.ignorecase true &&\n+\techo FILE > FILE &&\n+\tgit add FILE &&\n+\tgit commit -m add FILE &&\n+\t{\n+\t\tif ! test -f File; then\n+\t\t\tln FILE File\n+\t\tfi\n+\t} &&\n+\tgit mv FILE File &&\n+\tgit commit -m \"mv FILE File\" &&\n+\trm -f FILE File &&\n+\tgit reset --hard &&\n+\ttest \"$(echo *)\" = File\n+'\n+\n+\n test_done\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex abc47f3..8c71583 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -1080,6 +1080,10 @@ fi\n # test whether the filesystem supports symbolic links\n ln -s x y 2>/dev/null && test -h y 2>/dev/null && test_set_prereq SYMLINKS\n rm -f y\n+# test whether the filesystem supports hard links\n+>x\n+ln x y 2>/dev/null && test -f y 2>/dev/null && test_set_prereq HARDLINKS\n+rm -f x y\n \n # When the tests are run as root, permission tests will report that\n # things are writable when they shouldn't be.\n-- \n1.7.4.3\n"},{"id":"165797","messageId":"1B0C04AB-0710-43A9-8046-C08593752BDE@gmail.com","threadId":"27047","inReplyTo":"201104100750.29950.tboegi@web.de","subject":"Re: [PATCH] Allow git mv FILENAME Filename when core.ignorecase = true","fromName":"Joshua Juran","fromEmail":"jjuran@gmail.com","sentAt":"2011-04-14T05:39:26Z","receivedAt":"2011-04-14T05:39:26Z","isPatch":true,"sender":{"key":"jjuran@gmail.com","avatar":null},"body":"On Apr 9, 2011, at 10:50 PM, Torsten Bögershausen wrote:\n\n> The following discussion assumes, that we are on a\n> \"case ignoring\" file system, and core.ignorecase = true.\n\nBoth commas should be removed.\n\nJosh\n"}]}