{"thread":{"id":"26662","subject":"[PATCH] Allow git mv FileA fILEa when core.ignorecase = true","startedAt":"2011-03-04T21:40:38Z","lastAt":"2011-03-19T14:28:28Z","messageCount":4,"participants":["Torsten Bögershausen","Erik Faye-Lund"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"162795","messageId":"201103042240.38969.tboegi@web.de","threadId":"26662","inReplyTo":null,"subject":"[PATCH] Allow git mv FileA fILEa when core.ignorecase = true","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2011-03-04T21:40:38Z","receivedAt":"2011-03-04T21:40:38Z","isPatch":true,"sender":{"key":"tboegi@web.de","avatar":"https://avatars.githubusercontent.com/u/7138363?v=4"},"body":"    The typical use case is when a file \"FileA\" should be renamed into fILEa\n    and we are on a case insenstive file system (system core.ignorecase = true).\n    Source and destination are the same file, it can be accessed under both names.\n    This makes git think that the destination file exists.\n    Unless used with --forced, git will refuse the \"git mv FileA fILEa\".\n    This change will allow \"git mv FileA fILEa\", when core.ignorecase = true\n    and source and destination filenames only differ in case and the file length\n    is identical.\n    On Linux/Unix/Mac OS X the mv is allowed when the inode of the source and\n    destination are equal.\n    On  this allows renames of MÄRCHEN into Märchen on Mac OS X.\n    (As a side effect, a file can be renamed to a name which is already\n    hard-linked to the same inode)\n\nSigned-off-by: Torsten Bögershausen <tboegi@web.de>\n---\n builtin/mv.c  |   20 +++++++++++++++-----\n t/t7001-mv.sh |   29 +++++++++++++++++++++++++++++\n 2 files changed, 44 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex 93e8995..e0aad62 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -62,7 +62,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t};\n \tconst char **source, **destination, **dest_path;\n \tenum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes;\n-\tstruct stat st;\n+\tstruct stat st, st_dst;\n \tstruct string_list src_for_dst = STRING_LIST_INIT_NODUP;\n \n \tgit_config(git_default_config, NULL);\n@@ -164,15 +164,25 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n \t\t\t}\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\telse if (lstat(dst, &st_dst) == 0) {\n+\t\t\tint allow_force = force;\n \t\t\tbad = \"destination exists\";\n-\t\t\tif (force) {\n+\t\t\t/* Allow when src and dst have the same inode (Mac OS X) */\n+\t\t\t/* Allow when ignore_case and same file length (Windows) */\n+\t\t\tif (((st_dst.st_ino) && (st_dst.st_ino == st.st_ino)) ||\n+\t\t\t\t\t((ignore_case) && !strcasecmp(src, dst) &&\n+\t\t\t\t\t (st.st_size == st_dst.st_size))) {\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\tif (S_ISREG(st_dst.st_mode) || S_ISLNK(st_dst.st_mode)) {\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/t/t7001-mv.sh b/t/t7001-mv.sh\nindex a845b15..d0e73ee 100755\n--- a/t/t7001-mv.sh\n+++ b/t/t7001-mv.sh\n@@ -255,4 +255,33 @@ test_expect_success SYMLINKS 'git mv should overwrite file with a symlink' '\n \n rm -f moved symlink\n \n+touch x\n+if ln x y 2>/dev/null; then\n+\thardlinks=1\n+fi\n+rm -f x y\n+\n+if test \"$(git config --bool core.ignorecase)\" = true -o \"$hardlinks\"; then\n+\ttest_expect_success 'git mv FileA fILEa' '\n+\n+\t\trm -fr .git * &&\n+\t\tgit init &&\n+\t\techo FileA > FileA &&\n+\t\tgit add FileA &&\n+\t\tgit commit -m add FileA &&\n+\t\t{\n+\t\t\tif ! test -f fILEa; then\n+\t\t\t\tln FileA fILEa\n+\t\t\tfi\n+\t\t} &&\n+\t\tgit mv FileA fILEa &&\n+\t\tgit commit -m \"mv FileA fILEa\" &&\n+\t\trm -f FileA fILEa &&\n+\t\tgit reset --hard &&\n+\t\ttest \"$(echo *)\" = fILEa\n+\t'\n+else\n+\tsay \"Neither ignorecase nor hardlinks, skipping git mv FileA fILEa\"\n+fi\n+\n test_done\n-- \n1.7.4\n"},{"id":"163469","messageId":"AANLkTin2s_=TVaKx6yv+nDvXB41BxebWHX4iaDYSGd-e@mail.gmail.com","threadId":"26662","inReplyTo":"201103042240.38969.tboegi@web.de","subject":"Re: [PATCH] Allow git mv FileA fILEa when core.ignorecase = true","fromName":"Erik Faye-Lund","fromEmail":"kusmabite@gmail.com","sentAt":"2011-03-16T13:05:04Z","receivedAt":"2011-03-16T13:05:04Z","isPatch":true,"sender":{"key":"kusmabite@gmail.com","avatar":"https://avatars.githubusercontent.com/u/47073?v=4"},"body":"2011/3/4 Torsten Bögershausen <tboegi@web.de>:\n>    The typical use case is when a file \"FileA\" should be renamed into fILEa\n>    and we are on a case insenstive file system (system core.ignorecase = true).\n>    Source and destination are the same file, it can be accessed under both names.\n>    This makes git think that the destination file exists.\n>    Unless used with --forced, git will refuse the \"git mv FileA fILEa\".\n>    This change will allow \"git mv FileA fILEa\", when core.ignorecase = true\n>    and source and destination filenames only differ in case and the file length\n>    is identical.\n>    On Linux/Unix/Mac OS X the mv is allowed when the inode of the source and\n>    destination are equal.\n>    On  this allows renames of MÄRCHEN into Märchen on Mac OS X.\n>    (As a side effect, a file can be renamed to a name which is already\n>    hard-linked to the same inode)\n>\n> Signed-off-by: Torsten Bögershausen <tboegi@web.de>\n> ---\n>  builtin/mv.c  |   20 +++++++++++++++-----\n>  t/t7001-mv.sh |   29 +++++++++++++++++++++++++++++\n>  2 files changed, 44 insertions(+), 5 deletions(-)\n>\n> diff --git a/builtin/mv.c b/builtin/mv.c\n> index 93e8995..e0aad62 100644\n> --- a/builtin/mv.c\n> +++ b/builtin/mv.c\n> @@ -62,7 +62,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>        };\n>        const char **source, **destination, **dest_path;\n>        enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes;\n> -       struct stat st;\n> +       struct stat st, st_dst;\n>        struct string_list src_for_dst = STRING_LIST_INIT_NODUP;\n>\n>        git_config(git_default_config, NULL);\n> @@ -164,15 +164,25 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>                        }\n>                } else if (cache_name_pos(src, length) < 0)\n>                        bad = \"not under version control\";\n> -               else if (lstat(dst, &st) == 0) {\n> +               else if (lstat(dst, &st_dst) == 0) {\n> +                       int allow_force = force;\n>                        bad = \"destination exists\";\n> -                       if (force) {\n> +                       /* Allow when src and dst have the same inode (Mac OS X) */\n> +                       /* Allow when ignore_case and same file length (Windows) */\n\nWait, what? Same file length is sufficient to trigger overwriting\nwithout -f? I find this to be a very dubious heuristic...\n\nShouldn't you be checking something like nFileIndexLow/High from\nBY_HANDLE_FILE_INFORMATION instead? (ref:\nhttp://msdn.microsoft.com/en-us/library/aa363788(v=VS.85).aspx)\n\nSure, we'd need some API to check that, but if we assume that this\ncode path is rare-ish we could do something like this (note,\nuntested):\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 6750e67..fee4113 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -299,6 +299,21 @@ void mingw_mark_as_git_dir(const char *dir)\n \t\t \"dotGitOnly\" : \"true\"));\n }\n\n+int is_same_file(const char *a, const char *b)\n+{\n+\tBY_HANDLE_FILE_INFORMATION hia, hib;\n+\tHANDLE ha = OpenFileA(a, NULL, OF_READ),\n+\t       hb = OpenFileA(b, NULL, OF_READ);\n+\tif (!ha || !hb ||\n+\t    !GetFileInformationByHandle(ha) ||\n+\t    !GetFileInformationByHandle(hb))\n+\t\treturn 0;\n+\n+\treturn hia.dwVolumeSerialNumber == hib.dwVolumeSerialNumber &&\n+\t    hia.nFileSizeLow == hib.nFileSizeLow &&\n+\t    hia.nFileSizeHigh == hib.nFileSizeHigh;\n+}\n+\n #undef mkdir\n int mingw_mkdir(const char *path, int mode)\n {\ndiff --git a/compat/mingw.h b/compat/mingw.h\nindex 9c00e75..43c0b5f 100644\n--- a/compat/mingw.h\n+++ b/compat/mingw.h\n@@ -297,6 +297,9 @@ void mingw_open_html(const char *path);\n void mingw_mark_as_git_dir(const char *dir);\n #define mark_as_git_dir mingw_mark_as_git_dir\n\n+int mingw_is_same_file(const char *a, const char *b);\n+#define is_same_file mingw_is_same_file\n+\n /*\n  * helpers\n  */\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex 2ca2fad..b95b9e2 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -558,6 +558,17 @@ int remove_or_warn(unsigned int mode, const char *path);\n #define mark_as_git_dir(x) /* noop */\n #endif\n\n+#ifndef is_same_file\n+static inline int mingw_is_same_file(const char *a, const char *b)\n+{\n+\tstruct stat sta, stb;\n+\tif (lstat(a, &sta) ||\n+\t    lstat(b, &stb))\n+\t\treturn 0;\n+\treturn sta.st_dev == stb.st_deb && sta.st_ino == stb.st_ino;\n+}\n+#endif\n+\n #ifndef get_home_directory\n #define get_home_directory() getenv(\"HOME\")\n #endif\n"},{"id":"163468","messageId":"AANLkTikvkzUKBCqygGJoUG3qXNDArXpEX0RM5_5dbnBD@mail.gmail.com","threadId":"26662","inReplyTo":"AANLkTin2s_=TVaKx6yv+nDvXB41BxebWHX4iaDYSGd-e@mail.gmail.com","subject":"Re: [PATCH] Allow git mv FileA fILEa when core.ignorecase = true","fromName":"Erik Faye-Lund","fromEmail":"kusmabite@gmail.com","sentAt":"2011-03-16T13:18:28Z","receivedAt":"2011-03-16T13:18:28Z","isPatch":true,"sender":{"key":"kusmabite@gmail.com","avatar":"https://avatars.githubusercontent.com/u/47073?v=4"},"body":"2011/3/16 Erik Faye-Lund <kusmabite@gmail.com>:\n> 2011/3/4 Torsten Bögershausen <tboegi@web.de>:\n>>    The typical use case is when a file \"FileA\" should be renamed into fILEa\n>>    and we are on a case insenstive file system (system core.ignorecase = true).\n>>    Source and destination are the same file, it can be accessed under both names.\n>>    This makes git think that the destination file exists.\n>>    Unless used with --forced, git will refuse the \"git mv FileA fILEa\".\n>>    This change will allow \"git mv FileA fILEa\", when core.ignorecase = true\n>>    and source and destination filenames only differ in case and the file length\n>>    is identical.\n>>    On Linux/Unix/Mac OS X the mv is allowed when the inode of the source and\n>>    destination are equal.\n>>    On  this allows renames of MÄRCHEN into Märchen on Mac OS X.\n>>    (As a side effect, a file can be renamed to a name which is already\n>>    hard-linked to the same inode)\n>>\n>> Signed-off-by: Torsten Bögershausen <tboegi@web.de>\n>> ---\n>>  builtin/mv.c  |   20 +++++++++++++++-----\n>>  t/t7001-mv.sh |   29 +++++++++++++++++++++++++++++\n>>  2 files changed, 44 insertions(+), 5 deletions(-)\n>>\n>> diff --git a/builtin/mv.c b/builtin/mv.c\n>> index 93e8995..e0aad62 100644\n>> --- a/builtin/mv.c\n>> +++ b/builtin/mv.c\n>> @@ -62,7 +62,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>>        };\n>>        const char **source, **destination, **dest_path;\n>>        enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes;\n>> -       struct stat st;\n>> +       struct stat st, st_dst;\n>>        struct string_list src_for_dst = STRING_LIST_INIT_NODUP;\n>>\n>>        git_config(git_default_config, NULL);\n>> @@ -164,15 +164,25 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>>                        }\n>>                } else if (cache_name_pos(src, length) < 0)\n>>                        bad = \"not under version control\";\n>> -               else if (lstat(dst, &st) == 0) {\n>> +               else if (lstat(dst, &st_dst) == 0) {\n>> +                       int allow_force = force;\n>>                        bad = \"destination exists\";\n>> -                       if (force) {\n>> +                       /* Allow when src and dst have the same inode (Mac OS X) */\n>> +                       /* Allow when ignore_case and same file length (Windows) */\n>\n> Wait, what? Same file length is sufficient to trigger overwriting\n> without -f? I find this to be a very dubious heuristic...\n>\n> Shouldn't you be checking something like nFileIndexLow/High from\n> BY_HANDLE_FILE_INFORMATION instead? (ref:\n> http://msdn.microsoft.com/en-us/library/aa363788(v=VS.85).aspx)\n>\n> Sure, we'd need some API to check that, but if we assume that this\n> code path is rare-ish we could do something like this (note,\n> untested):\n>\n> diff --git a/compat/mingw.c b/compat/mingw.c\n> index 6750e67..fee4113 100644\n> --- a/compat/mingw.c\n> +++ b/compat/mingw.c\n> @@ -299,6 +299,21 @@ void mingw_mark_as_git_dir(const char *dir)\n>                 \"dotGitOnly\" : \"true\"));\n>  }\n>\n> +int is_same_file(const char *a, const char *b)\n> +{\n> +       BY_HANDLE_FILE_INFORMATION hia, hib;\n> +       HANDLE ha = OpenFileA(a, NULL, OF_READ),\n> +              hb = OpenFileA(b, NULL, OF_READ);\n> +       if (!ha || !hb ||\n> +           !GetFileInformationByHandle(ha) ||\n> +           !GetFileInformationByHandle(hb))\n> +               return 0;\n> +\n\nAnd if couse:\nCloseHandle(ha);\nCloseHandle(hb);\n"},{"id":"163749","messageId":"4D84BD8C.7020906@web.de","threadId":"26662","inReplyTo":"AANLkTikvkzUKBCqygGJoUG3qXNDArXpEX0RM5_5dbnBD@mail.gmail.com","subject":"Re: [PATCH] Allow git mv FileA fILEa when core.ignorecase = true","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2011-03-19T14:28:28Z","receivedAt":"2011-03-19T14:28:28Z","isPatch":true,"sender":{"key":"tboegi@web.de","avatar":"https://avatars.githubusercontent.com/u/7138363?v=4"},"body":"On 16.03.11 14:18, Erik Faye-Lund wrote:\n> 2011/3/16 Erik Faye-Lund <kusmabite@gmail.com>:\n>> 2011/3/4 Torsten Bögershausen <tboegi@web.de>:\n>>>    The typical use case is when a file \"FileA\" should be renamed into fILEa\n>>>    and we are on a case insenstive file system (system core.ignorecase = true).\n>>>    Source and destination are the same file, it can be accessed under both names.\n>>>    This makes git think that the destination file exists.\n>>>    Unless used with --forced, git will refuse the \"git mv FileA fILEa\".\n>>>    This change will allow \"git mv FileA fILEa\", when core.ignorecase = true\n>>>    and source and destination filenames only differ in case and the file length\n>>>    is identical.\n>>>    On Linux/Unix/Mac OS X the mv is allowed when the inode of the source and\n>>>    destination are equal.\n>>>    On  this allows renames of MÄRCHEN into Märchen on Mac OS X.\n>>>    (As a side effect, a file can be renamed to a name which is already\n>>>    hard-linked to the same inode)\n>>>\n>>> Signed-off-by: Torsten Bögershausen <tboegi@web.de>\n>>> ---\n>>>  builtin/mv.c  |   20 +++++++++++++++-----\n>>>  t/t7001-mv.sh |   29 +++++++++++++++++++++++++++++\n>>>  2 files changed, 44 insertions(+), 5 deletions(-)\n>>>\n>>> diff --git a/builtin/mv.c b/builtin/mv.c\n>>> index 93e8995..e0aad62 100644\n>>> --- a/builtin/mv.c\n>>> +++ b/builtin/mv.c\n>>> @@ -62,7 +62,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>>>        };\n>>>        const char **source, **destination, **dest_path;\n>>>        enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes;\n>>> -       struct stat st;\n>>> +       struct stat st, st_dst;\n>>>        struct string_list src_for_dst = STRING_LIST_INIT_NODUP;\n>>>\n>>>        git_config(git_default_config, NULL);\n>>> @@ -164,15 +164,25 @@ int cmd_mv(int argc, const char **argv, const char *prefix)\n>>>                        }\n>>>                } else if (cache_name_pos(src, length) < 0)\n>>>                        bad = \"not under version control\";\n>>> -               else if (lstat(dst, &st) == 0) {\n>>> +               else if (lstat(dst, &st_dst) == 0) {\n>>> +                       int allow_force = force;\n>>>                        bad = \"destination exists\";\n>>> -                       if (force) {\n>>> +                       /* Allow when src and dst have the same inode (Mac OS X) */\n>>> +                       /* Allow when ignore_case and same file length (Windows) */\n>>\n>> Wait, what? Same file length is sufficient to trigger overwriting\n>> without -f? I find this to be a very dubious heuristic...\n>>\n>> Shouldn't you be checking something like nFileIndexLow/High from\n>> BY_HANDLE_FILE_INFORMATION instead? (ref:\n>> http://msdn.microsoft.com/en-us/library/aa363788(v=VS.85).aspx)\n>>\n>> Sure, we'd need some API to check that, but if we assume that this\n>> code path is rare-ish we could do something like this (note,\n>> untested):\n>>\n>> diff --git a/compat/mingw.c b/compat/mingw.c\n>> index 6750e67..fee4113 100644\n>> --- a/compat/mingw.c\n>> +++ b/compat/mingw.c\n>> @@ -299,6 +299,21 @@ void mingw_mark_as_git_dir(const char *dir)\n>>                 \"dotGitOnly\" : \"true\"));\n>>  }\n>>\n>> +int is_same_file(const char *a, const char *b)\n>> +{\n>> +       BY_HANDLE_FILE_INFORMATION hia, hib;\n>> +       HANDLE ha = OpenFileA(a, NULL, OF_READ),\n>> +              hb = OpenFileA(b, NULL, OF_READ);\n>> +       if (!ha || !hb ||\n>> +           !GetFileInformationByHandle(ha) ||\n>> +           !GetFileInformationByHandle(hb))\n>> +               return 0;\n>> +\n> \n> And if couse:\n> CloseHandle(ha);\n> CloseHandle(hb);\nGood point. I will send a new patch, including your suggestion,\nbut 50% different ;-)\n/Torsten\n"}]}