{"thread":{"id":"26362","subject":"[PATCH] Handle rename of case only, for Windows","startedAt":"2011-01-29T23:45:17Z","lastAt":"2011-02-10T18:58:14Z","messageCount":5,"participants":["Tim Abell","René Scharfe","Jonathan Nieder","Ramsay Jones"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"160041","messageId":"1296344717.25999.1417928123@webmail.messagingengine.com","threadId":"26362","inReplyTo":null,"subject":"[PATCH] Handle rename of case only, for Windows","fromName":"Tim Abell","fromEmail":"tim@timwise.co.uk","sentAt":"2011-01-29T23:45:17Z","receivedAt":"2011-01-29T23:45:17Z","isPatch":true,"sender":{"key":"tim@timwise.co.uk","avatar":"https://gravatar.com/avatar/0d3f826b8cc4813b0a373fbfaf402be87a89e01328c16065e5762406b8066fee?d=mp&s=160"},"body":">From ddab003ede9f25d93f4e7eea833523a0aa29d16d Mon Sep 17 00:00:00 2001\nFrom: Tim Abell <tim@timwise.co.uk>\nDate: Thu, 27 Jan 2011 22:53:31 +0000\nSubject: [PATCH] Handle rename of case only, for Windows\n\nAdded test to show rename failing in windows.\nAdded test to show that this patch doesn't break protection against overwriting\nan existing file, and another to show that \"mv --force\" still works.\n\nAltered mv.c to check whether the source and destination file are actually\nthe same file based on inode number before failing.\nIf the inode is zero then the data is no use so old behaviour is maintained.\n\nA message from Linus on the subject:\n http://kerneltrap.org/mailarchive/git/2008/3/21/1220814\n (apparently he never got the energy :-)\n\nSigned-off-by: Tim Abell <tim@timwise.co.uk>\n---\n\nHi folks, this is my second attempt at providing a useful patch for this issue.\nThanks for all the great feedback from my first attempt. I've attempted to address the problems raised.\n\nHopefully my commit message is more like what is needed this time.\nI hadn't realised before that you can split the commit message from this bit with the \"---\".\n\n>Hmm, not so good. st_ino is always 0 on Windows, so this would make\n>false positives, no?\n\nI tested this on windows 7 under cygwin (which is what I have available) and st_ino reports real numbers for me, I also tested that attempting to overwrite another file without --force still fails and added a new test case for this scenario. I have now made sure that if zero is returned then git won't accidentally overwrite other files as I hadn't thought of this before. So this patch shouldn't be regressive even if other versions of windows or other filesystems don't provide valid inode data.\n\nAdding the following line after \"lstat(src, &src_st);\" shows the inode numbers when calling git mv:\n printf(\"src inode: %lu, dst inode: %lu\", (unsigned long) src_st.st_ino, (unsigned long) st.st_ino);\nAnd here is my ouput showing it in action:\n\n$ git mv foo.txt bar.txt\n  fatal: destination exists, source=foo.txt, destination=bar.txt\n  src inode: 67064, dst inode: 229369\n$ git mv foo.txt Foo.txt\n  src inode: 67064, dst inode: 67064\n\n>I wonder if we can make lstat_case() that would only return 0 if it\n>matches exactly the filename, even on FAT. FindFirstFile/FindNextFile\n>should return true file name, I think. If not, we can make\n>lstat_case() take two paths (src and dst) and move all inode\n>comparison code in there. Much cleaner.\n\nI'm afraid this is a bit beyond me at the moment, but I'm fairly happy with the solution I have. Thanks for the feedback though.\n\n>Couldn't this be moved inside the scope around \"cache_name_pos\"?\n>That's the only scope it is valid inside anyway...\n\nDone. I wasn't sure about this initially as I'm not very experienced with C programming. Thanks for the pointer.\n\n>Perhaps you could use the full URL (and maybe put it in the commit\n>message insted)? It'd be nice if we could reach this information even\n>if is.gd disappears...\n\nGood point. I've put the full url in the commit message instead.\n\n>Uhm, is this debug-leftovers? #warning is a preprocessor-construct,\n>and it can't understand varaibles in c. Especially not formatted as\n>strings. Can #warning even do varags? :P\n\nYes, it was a debug line. Doh! Complete chance that it compiled, I've been doing too much bash scripting recently it seems. I've stripped it out. A better version is available above should anyone want to see the inode numbers.\n\nThanks all, I welcome any more feedback.\n\nDoes this now look like something that anyone on the git project would like to pick up as a contribution?\n\nYours\n\nTim Abell\n\n\n builtin/mv.c  |   32 +++++++++++++++++++++-----------\n t/t7001-mv.sh |   35 +++++++++++++++++++++++++++++++++++\n 2 files changed, 56 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/mv.c b/builtin/mv.c\nindex cdbb094..c2f726a 100644\n--- a/builtin/mv.c\n+++ b/builtin/mv.c\n@@ -165,17 +165,27 @@ 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\tbad = \"destination exists\";\n-\t\t\tif (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\tbad = NULL;\n-\t\t\t\t} else\n-\t\t\t\t\tbad = \"Cannot overwrite\";\n+\t\t\t/* If we are on a case insensitive file system (windows) and we are only\n+\t\t\t * changing the case of the file then lstat for the destination will\n+\t\t\t * return != 0 because it sees the source file.\n+\t\t\t * To prevent this causing failure, lstat is used to get the inode of the src\n+\t\t\t * and see if it's actually the same file as dst. If the inode == 0 then\n+\t\t\t * we can't tell whether it is the same file so we fail regardless. */\n+\t\t\tstruct stat src_st;\n+\t\t\tlstat(src, &src_st);\n+\t\t\tif (src_st.st_ino == 0 || src_st.st_ino != st.st_ino) {\n+\t\t\t\tbad = \"destination exists\";\n+\t\t\t\tif (force) {\n+\t\t\t\t\t/*\n+\t\t\t\t\t * only files can overwrite each other:\n+\t\t\t\t\t * check both source and destination\n+\t\t\t\t\t */\n+\t\t\t\t\tif (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {\n+\t\t\t\t\t\twarning(\"%s; will overwrite!\", bad);\n+\t\t\t\t\t\tbad = NULL;\n+\t\t\t\t\t} else\n+\t\t\t\t\t\tbad = \"Cannot overwrite\";\n+\t\t\t\t}\n \t\t\t}\n \t\t} else if (string_list_has_string(&src_for_dst, dst))\n \t\t\tbad = \"multiple sources for the same target\";\ndiff --git a/t/t7001-mv.sh b/t/t7001-mv.sh\nindex 65a35d9..abaecb6 100755\n--- a/t/t7001-mv.sh\n+++ b/t/t7001-mv.sh\n@@ -255,4 +255,39 @@ test_expect_success SYMLINKS 'git mv should overwrite file with a symlink' '\n \n rm -f moved symlink\n \n+test_expect_success 'git mv should not fail when only changing case' '\n+\n+\trm -fr .git &&\n+\tgit init &&\n+\t>foo.txt &&\n+\tgit add foo.txt &&\n+\tgit mv foo.txt Foo.txt\n+'\n+\n+rm *.txt\n+\n+test_expect_success 'git mv should not overwrite existing file' '\n+\n+\trm -fr .git &&\n+\tgit init &&\n+\t>foo.txt &&\n+\t>bar.txt &&\n+\tgit add foo.txt &&\n+\ttest_must_fail git mv foo.txt bar.txt\n+'\n+\n+rm *.txt\n+\n+test_expect_success 'git mv -f should overwrite existing file' '\n+\n+\trm -fr .git &&\n+\tgit init &&\n+\t>foo.txt &&\n+\t>bar.txt &&\n+\tgit add foo.txt &&\n+\tgit mv -f foo.txt bar.txt\n+'\n+\n+rm *.txt\n+\n test_done\n-- \n1.7.3.5.3.g2b35a\n"},{"id":"160042","messageId":"4D44CB60.8000506@lsrfire.ath.cx","threadId":"26362","inReplyTo":"1296344717.25999.1417928123@webmail.messagingengine.com","subject":"Re: [PATCH] Handle rename of case only, for Windows","fromName":"René Scharfe","fromEmail":"rene.scharfe@lsrfire.ath.cx","sentAt":"2011-01-30T02:22:24Z","receivedAt":"2011-01-30T02:22:24Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 30.01.2011 00:45, schrieb Tim Abell:\n>> Hmm, not so good. st_ino is always 0 on Windows, so this would make\n>> false positives, no?\n> \n> I tested this on windows 7 under cygwin (which is what I have\n> available) and st_ino reports real numbers for me, I also tested that\n> attempting to overwrite another file without --force still fails and\n> added a new test case for this scenario. I have now made sure that if\n> zero is returned then git won't accidentally overwrite other files as\n> I hadn't thought of this before. So this patch shouldn't be\n> regressive even if other versions of windows or other filesystems\n> don't provide valid inode data.\n\nOn MinGW on Vista st_ino is zero, git mv refuses to overwrite and the\nadded case change test fails as a consequence.\n\n>> I wonder if we can make lstat_case() that would only return 0 if it\n>> matches exactly the filename, even on FAT. FindFirstFile/FindNextFile\n>> should return true file name, I think. If not, we can make\n>> lstat_case() take two paths (src and dst) and move all inode\n>> comparison code in there. Much cleaner.\n> \n> I'm afraid this is a bit beyond me at the moment, but I'm fairly\n> happy with the solution I have. Thanks for the feedback though.\n\nHmm, if the patch only works for Cygwin and maybe OS X it's a step\nforward, I guess.  A more complete solution would be better, of course.\n\n> diff --git a/builtin/mv.c b/builtin/mv.c\n> index cdbb094..c2f726a 100644\n> --- a/builtin/mv.c\n> +++ b/builtin/mv.c\n> @@ -165,17 +165,27 @@ 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\tbad = \"destination exists\";\n> -\t\t\tif (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\tbad = NULL;\n> -\t\t\t\t} else\n> -\t\t\t\t\tbad = \"Cannot overwrite\";\n> +\t\t\t/* If we are on a case insensitive file system (windows) and we are only\n> +\t\t\t * changing the case of the file then lstat for the destination will\n> +\t\t\t * return != 0 because it sees the source file.\n> +\t\t\t * To prevent this causing failure, lstat is used to get the inode of the src\n> +\t\t\t * and see if it's actually the same file as dst. If the inode == 0 then\n> +\t\t\t * we can't tell whether it is the same file so we fail regardless. */\n\nCan you make the lines 80 characters long at most?  This subtly helps\navoiding excessive levels of indentation by encouraging to factor out\nnice and small functions.\n\n> +\t\t\tstruct stat src_st;\n> +\t\t\tlstat(src, &src_st);\n\nShouldn't you check the return value of this call?  OK, the source\nprobably always exists, but still.  Oh, we actually know that because\nthat's the first lstat() call in this for loop.  You can reuse its\nresult instead of calling the function again.\n\n> +\t\t\tif (src_st.st_ino == 0 || src_st.st_ino != st.st_ino) {\n\nIt may be nice to avoid adding another level of indentation by combining\nthis if with the preceding one.\n\n> +\t\t\t\tbad = \"destination exists\";\n> +\t\t\t\tif (force) {\n> +\t\t\t\t\t/*\n> +\t\t\t\t\t * only files can overwrite each other:\n> +\t\t\t\t\t * check both source and destination\n> +\t\t\t\t\t */\n> +\t\t\t\t\tif (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {\n> +\t\t\t\t\t\twarning(\"%s; will overwrite!\", bad);\n> +\t\t\t\t\t\tbad = NULL;\n> +\t\t\t\t\t} else\n> +\t\t\t\t\t\tbad = \"Cannot overwrite\";\n> +\t\t\t\t}\n>  \t\t\t}\n>  \t\t} else if (string_list_has_string(&src_for_dst, dst))\n>  \t\t\tbad = \"multiple sources for the same target\";\n\nRené\n"},{"id":"160054","messageId":"1296406435.8170.1418006125@webmail.messagingengine.com","threadId":"26362","inReplyTo":"4D44CB60.8000506@lsrfire.ath.cx","subject":"Re: [PATCH] Handle rename of case only, for Windows","fromName":"Tim Abell","fromEmail":"tim@timwise.co.uk","sentAt":"2011-01-30T16:53:55Z","receivedAt":"2011-01-30T16:53:55Z","isPatch":true,"sender":{"key":"tim@timwise.co.uk","avatar":"https://gravatar.com/avatar/0d3f826b8cc4813b0a373fbfaf402be87a89e01328c16065e5762406b8066fee?d=mp&s=160"},"body":"> +\t\t\tstruct stat src_st;\n> +\t\t\tlstat(src, &src_st);\n\nShouldn't you check the return value of this call?  OK, the source\nprobably always exists, but still.  Oh, we actually know that because\nthat's the first lstat() call in this for loop.  You can reuse its\nresult instead of calling the function again.\n\nThanks for your feedback, you are undoubtedly right about checking the return value, I hadn't though of that. Just out of interest I tried moving a file with git mv that i'd removed from the filesystem. Here's the result, so at least it's not dying horribly:\n\n  $ rm foo.txt\n\n  $ git status\n  # On branch master\n  # Changed but not updated:\n  #   (use \"git add/rm <file>...\" to update what will be committed)\n  #   (use \"git checkout -- <file>...\" to discard changes in working directory)\n  #\n  #       deleted:    foo.txt\n  #\n  no changes added to commit (use \"git add\" and/or \"git commit -a\")\n\n  $ git mv foo.txt bar.txt\n  fatal: bad source, source=foo.txt, destination=bar.txt\n\n--\n\nWhat do I need to do from here to make my patch acceptable/useful to git's maintainers?\n\nThanks\n\nTim Abell\n"},{"id":"160069","messageId":"20110130213931.GA11285@burratino","threadId":"26362","inReplyTo":"1296406435.8170.1418006125@webmail.messagingengine.com","subject":"Re: [PATCH] Handle rename of case only, for Windows","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2011-01-30T21:39:31Z","receivedAt":"2011-01-30T21:39:31Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Hi Tim,\n\nTim Abell wrote:\n\n> Thanks for your feedback, you are undoubtedly right about checking\n> the return value\n[...]\n> What do I need to do from here to make my patch acceptable/useful to\n> git's maintainers?\n\nGeneric answer: see Documentation/SubmittingPatches, section labelled\n\"An ideal patch flow\".  Basically you (or anyone else interested it\ngetting it accepted) are the maintainer of the patch, so you can make\nimprovements until you and others are happy with it.\n\nHope that helps,\nJonathan\n"},{"id":"160831","messageId":"4D543546.1030207@ramsay1.demon.co.uk","threadId":"26362","inReplyTo":"1296344717.25999.1417928123@webmail.messagingengine.com","subject":"Re: [PATCH] Handle rename of case only, for Windows","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsay1.demon.co.uk","sentAt":"2011-02-10T18:58:14Z","receivedAt":"2011-02-10T18:58:14Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"[Sorry for the late reply to this... I'm somewhat backed-up!]\n\nTim Abell wrote:\n>>From ddab003ede9f25d93f4e7eea833523a0aa29d16d Mon Sep 17 00:00:00 2001\n> From: Tim Abell <tim@timwise.co.uk>\n> Date: Thu, 27 Jan 2011 22:53:31 +0000\n> Subject: [PATCH] Handle rename of case only, for Windows\n\nThis should not work with the MinGW or msvc build, since they *always* set\nthe st_ino to zero (see do_lstat() and mingw_fstat() in compat/mingw.c).\nAlso, *with default configuration*, it will also not work on cygwin... so\nsaying that this works on Windows may be overstating things a bit ... ;-)\n\n[Hmm, I can't check, but it would probably work on Mac OS X ...]\n\n> Added test to show rename failing in windows.\n\nThis fails for me. (er... just to be clear, test #30 fails with your patch\napplied).\n\n> Added test to show that this patch doesn't break protection against overwriting\n> an existing file, and another to show that \"mv --force\" still works.\n> \n> Altered mv.c to check whether the source and destination file are actually\n> the same file based on inode number before failing.\n> If the inode is zero then the data is no use so old behaviour is maintained.\n\nWith default configuration, the st_ino will always be zero on cygwin. (see for\nexample the \"schizophrenic l/stat() functions\" commits adbc0b6, 7faee6b and\n7974843, along with the \"force core.filemode\" commit c869753).\n\nSo, you must have core.filemode or core.ignorecygwinfstricks set somewhere in\nyour environment (in the system or global (user) config files?).\n\n[I *always* set core.filemode after git-init or git-clone, so this patch may\nactually work for me *in practice* (I haven't tested it), but I may well not\nbe a typical user...]\n\n>> Hmm, not so good. st_ino is always 0 on Windows, so this would make\n>> false positives, no?\n> \n> I tested this on windows 7 under cygwin (which is what I have available) and st_ino reports real numbers for me,\n\nAgain, you must have non-default configuration set...\n\nATB,\nRamsay Jones\n"}]}