git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
From: "Torsten Bögershausen" <tboegi@web.de>
To: git@vger.kernel.org
Cc: tboegi@web.de
Subject: [PATCH] Allow git mv FILENAME Filename when core.ignorecase = true
Date: Sun, 10 Apr 2011 07:50:29 +0200	[thread overview]
Message-ID: <201104100750.29950.tboegi@web.de> (raw)

Motivation:
The typical use case is when a file named "FILENAME" should be
renamed into "Filename" and we are on a case ignoring file system
(core.ignorecase = true).

Using "mv FILENAME Filename" outside git succeeds,
(on Windows and MAC OS X, under Linux the mv command rejects
"mv: `Filename' and `FILENAME' are the same file").

"git mv FILENAME Filename" is refused, "fatal: destination exists",
unless "git mv --forced FILENAME Filename" is used.
The underlying file system makes git think that the
destination "Filename" exists.

The following discussion assumes, that we are on a
"case ignoring" file system, and core.ignorecase = true.

This change allows "git mv FILENAME Filename".
Using non ASCII works as well, like "git mv MÄRCHEN Märchen".
The ambition is that "git mv FILENAME Filename" changes both
the git index and the filename in the working tree,
in the same way how "git mv Filename NewFile" works.
Note: Under Linux+vfat The rename() function does not the rename
in the working directory.

Implementation details:
A possible approach to allow the "git mv FILENAME Filename"
is to compare both file names using strcasecmp().

This works for filenames where all characters are ASCII,
It will fail for "git mv MÄRCHEN Märchen".

Git has now idea about the encoding of filenames
(like UTF-8, ISO-8859-1 or any other).
Neither has strcasecmp() an idea how to handle non ASCII characters.

With this patch git lets the underlying file system decide
if 2 file names refer to the same file.

Remember that the file system does this already, by returning the
same values for lstat("FILENAME") and lstat("Filename").

By comparing all members in "struct stat" we can be sure that
both filenames point out the same file.
This is done in the function "equivalent_filenames()".

As lstat() on Windows (mingw.c or cygwin.c) sets st_ino to 0,
(and st_dev and other fields in struct stat)
we need other checks when running under Windows.

Therefore a different implementation of equivalent_filenames() is used
under Windows.
It uses GetFileInformationByHandle() to get and compare
dwVolumeSerialNumber, nFileIndexLow and nFileIndexHigh.
It uses even lstat(), since Windows reports the same nFileIndexLow/High for
a file and a softlink (under cygwin) pointing to it.

To summarize:
equivalent_filenames() is OS specific and checks under Windows:
dwVolumeSerialNumber, nFileIndexLow/High, st_mode, st_size,
st_atime and st_mtime.
All other OS check
st_mode, st_dev, st_ino, st_uid, st_gid, st_size, st_atime, st_mtime.

As a bonus (or regression), a file name can be renamed to a file name
which is already hard-linked to the same inode.

Signed-off-by: Torsten Bögershausen <tboegi@web.de>
---
 builtin/mv.c      |   10 +++++++-
 compat/cygwin.c   |   44 ++++++++++++++++++++++++++++++++++++++++
 compat/cygwin.h   |    3 ++
 compat/mingw.c    |   33 ++++++++++++++++++++++++++++++
 compat/mingw.h    |    3 ++
 git-compat-util.h |   17 +++++++++++++++
 t/t7001-mv.sh     |   57 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 t/test-lib.sh     |    4 +++
 8 files changed, 169 insertions(+), 2 deletions(-)

diff --git a/builtin/mv.c b/builtin/mv.c
index 40f33ca..7be7d8a 100644
--- a/builtin/mv.c
+++ b/builtin/mv.c
@@ -165,14 +165,20 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
 		} else if (cache_name_pos(src, length) < 0)
 			bad = _("not under version control");
 		else if (lstat(dst, &st) == 0) {
+			int allow_force = force;
 			bad = _("destination exists");
-			if (force) {
+			if (!force && ignore_case && equivalent_filenames(src, dst)) {
+				allow_force = 1;
+				bad = NULL;
+			}
+			if (allow_force) {
 				/*
 				 * only files can overwrite each other:
 				 * check both source and destination
 				 */
 				if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {
-					warning(_("%s; will overwrite!"), bad);
+					if (bad)
+						warning(_("%s; will overwrite!"), bad);
 					bad = NULL;
 				} else
 					bad = _("Cannot overwrite");
diff --git a/compat/cygwin.c b/compat/cygwin.c
index b4a51b9..4fdd94a 100644
--- a/compat/cygwin.c
+++ b/compat/cygwin.c
@@ -1,6 +1,7 @@
 #define WIN32_LEAN_AND_MEAN
 #include "../git-compat-util.h"
 #include "win32.h"
+#include <io.h>
 #include "../cache.h" /* to read configuration */
 
 static inline void filetime_to_timespec(const FILETIME *ft, struct timespec *ts)
@@ -85,6 +86,49 @@ static int cygwin_stat(const char *path, struct stat *buf)
 	return do_stat(path, buf, stat);
 }
 
+int cygwin_equivalent_filenames(const char *a, const char *b)
+{
+	int fd;
+	BY_HANDLE_FILE_INFORMATION hia, hib;
+	HANDLE h;
+	struct stat st_a, st_b;
+
+	if (lstat(a, &st_a) || lstat(b, &st_b))
+		return 0;
+
+	fd = open(a, O_RDONLY);
+	if (-1 == fd)
+		return 0;
+
+	h = (HANDLE)get_osfhandle(fd);
+	if (INVALID_HANDLE_VALUE == h)
+		return 0;
+
+	if (!(GetFileInformationByHandle(h,&hia)))
+		return 0;
+	CloseHandle(h);
+	close(fd);
+
+	fd = open(b, O_RDONLY);
+	if (-1 == fd)
+		return 0;
+
+	h = (HANDLE)get_osfhandle(fd);
+	if (INVALID_HANDLE_VALUE == h)
+		return 0;
+	if (!(GetFileInformationByHandle(h,&hib)))
+		return 0;
+	CloseHandle(h);
+	close(fd);
+
+	return st_a.st_mode == st_b.st_mode &&
+	       st_a.st_size == st_b.st_size &&
+	       st_a.st_atime == st_b.st_atime &&
+	       st_a.st_mtime == st_b.st_mtime &&
+	       hia.dwVolumeSerialNumber == hib.dwVolumeSerialNumber &&
+	       hia.nFileIndexLow == hib.nFileIndexLow &&
+	       hia.nFileIndexHigh == hib.nFileIndexHigh;
+}
 
 /*
  * At start up, we are trying to determine whether Win32 API or cygwin stat
diff --git a/compat/cygwin.h b/compat/cygwin.h
index a3229f5..04cc17e 100644
--- a/compat/cygwin.h
+++ b/compat/cygwin.h
@@ -7,3 +7,6 @@ extern stat_fn_t cygwin_lstat_fn;
 
 #define stat(path, buf) (*cygwin_stat_fn)(path, buf)
 #define lstat(path, buf) (*cygwin_lstat_fn)(path, buf)
+
+int cygwin_equivalent_filenames(const char *a, const char *b);
+#define equivalent_filenames cygwin_equivalent_filenames
diff --git a/compat/mingw.c b/compat/mingw.c
index 878b1de..56be81a 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -474,6 +474,39 @@ int mingw_fstat(int fd, struct stat *buf)
 	return -1;
 }
 
+int mingw_equivalent_filenames(const char *a, const char *b)
+{
+	BY_HANDLE_FILE_INFORMATION hia, hib;
+	HANDLE h;
+	struct stat st_a, st_b;
+
+	if (lstat(a, &st_a) || lstat(b, &st_b))
+		return 0;
+
+	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 st_a.st_mode == st_b.st_mode &&
+	       st_a.st_size == st_b.st_size &&
+	       st_a.st_atime == st_b.st_atime &&
+	       st_a.st_mtime == st_b.st_mtime &&
+	       hia.dwVolumeSerialNumber == hib.dwVolumeSerialNumber &&
+	       hia.nFileIndexLow == hib.nFileIndexLow &&
+	       hia.nFileIndexHigh == hib.nFileIndexHigh;
+}
+
 static inline void time_t_to_filetime(time_t t, FILETIME *ft)
 {
 	long long winTime = t * 10000000LL + 116444736000000000LL;
diff --git a/compat/mingw.h b/compat/mingw.h
index 62eccd3..3445104 100644
--- a/compat/mingw.h
+++ b/compat/mingw.h
@@ -303,6 +303,9 @@ int winansi_fprintf(FILE *stream, const char *format, ...) __attribute__((format
 void mingw_open_html(const char *path);
 #define open_html mingw_open_html
 
+int mingw_equivalent_filenames(const char *a, const char *b);
+#define equivalent_filenames mingw_equivalent_filenames
+
 /*
  * helpers
  */
diff --git a/git-compat-util.h b/git-compat-util.h
index 40498b3..d66cffe 100644
--- a/git-compat-util.h
+++ b/git-compat-util.h
@@ -567,4 +567,21 @@ int rmdir_or_warn(const char *path);
  */
 int remove_or_warn(unsigned int mode, const char *path);
 
+#ifndef equivalent_filenames
+static inline int equivalent_filenames(const char *a, const char *b) {
+	struct stat st_a, st_b;
+	if (lstat(a, &st_a) || lstat(b, &st_b))
+		return 0;
+
+	return st_a.st_mode == st_b.st_mode &&
+	       st_a.st_dev == st_b.st_dev &&
+	       st_a.st_ino == st_b.st_ino &&
+	       st_a.st_uid == st_b.st_uid &&
+	       st_a.st_gid == st_b.st_gid &&
+	       st_a.st_size == st_b.st_size &&
+	       st_a.st_atime == st_b.st_atime &&
+	       st_a.st_mtime == st_b.st_mtime;
+}
+#endif
+
 #endif
diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh
index a845b15..0c4b96a 100755
--- a/t/t7001-mv.sh
+++ b/t/t7001-mv.sh
@@ -255,4 +255,61 @@ test_expect_success SYMLINKS 'git mv should overwrite file with a symlink' '
 
 rm -f moved symlink
 
+unset encoding
+ae_upper_asc=AE
+ae_lower_asc=ae
+ae_upper_utf8=$(printf '\303\206')
+ae_lower_utf8=$(printf '\303\246')
+
+for enc in utf8 asc ; do
+	eval ae_lower=\$ae_lower_$enc
+	eval ae_upper=\$ae_upper_$enc
+	if (>./$ae_lower && echo broken > ./$ae_upper && test x"$(cat $ae_lower)" = xbroken ) 2>/dev/null ; then
+		if err=$(mv $ae_lower $ae_upper 2>&1); then
+			unset err
+			encoding=$enc
+			break
+		fi
+	else
+		err="case sensitive file system"
+	fi
+done
+
+if test -n "$encoding"; then
+	test_expect_success "git mv AE ae $encoding" '
+		rm -fr .git * &&
+		git init &&
+		echo $encoding > $ae_upper &&
+		git add $ae_upper &&
+		git commit -m "add AE" &&
+		git mv $ae_upper $ae_lower &&
+		git commit -m "mv AE ae" &&
+		rm -f $ae_upper $ae_lower &&
+		git reset --hard &&
+		test "$(echo *)" = $ae_lower
+	'
+else
+	say "Skipping 'git mv AE ae' $err ($enc)"
+fi
+
+test_expect_success HARDLINKS 'git mv FILE File HARDLINKED' '
+	rm -fr .git * &&
+	git init &&
+	git config core.ignorecase true &&
+	echo FILE > FILE &&
+	git add FILE &&
+	git commit -m add FILE &&
+	{
+		if ! test -f File; then
+			ln FILE File
+		fi
+	} &&
+	git mv FILE File &&
+	git commit -m "mv FILE File" &&
+	rm -f FILE File &&
+	git reset --hard &&
+	test "$(echo *)" = File
+'
+
+
 test_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
index abc47f3..8c71583 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -1080,6 +1080,10 @@ fi
 # test whether the filesystem supports symbolic links
 ln -s x y 2>/dev/null && test -h y 2>/dev/null && test_set_prereq SYMLINKS
 rm -f y
+# test whether the filesystem supports hard links
+>x
+ln x y 2>/dev/null && test -f y 2>/dev/null && test_set_prereq HARDLINKS
+rm -f x y
 
 # When the tests are run as root, permission tests will report that
 # things are writable when they shouldn't be.
-- 
1.7.4.3

             reply	other threads:[~2011-04-10  5:50 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2011-04-10  5:50 Torsten Bögershausen [this message]
2011-04-14  5:39 ` [PATCH] Allow git mv FILENAME Filename when core.ignorecase = true Joshua Juran

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://vger.kernel.org/majordomo-info.html

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=201104100750.29950.tboegi@web.de \
    --to=tboegi@web.de \
    --cc=git@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).