changeset 10218:750b7a4f01f6 stable

Add support for relinking on Windows. Test and minor code change by Patrick Mézard <pmezard@gmail.com>
author Siddharth Agarwal <sid.bugzilla@gmail.com>
date Fri, 08 Jan 2010 18:48:39 +0530
parents 2bbb4c8eb27e
children 3b94120864fc
files hgext/relink.py mercurial/posix.py mercurial/win32.py tests/test-relink tests/test-relink.out
diffstat 5 files changed, 74 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/hgext/relink.py	Fri Jan 08 22:30:07 2010 +0100
+++ b/hgext/relink.py	Fri Jan 08 18:48:39 2010 +0530
@@ -34,6 +34,8 @@
     Do not attempt any read operations on this repository while the command is
     running. (Both repositories will be locked against writes.)
     """
+    if not hasattr(util, 'samefile') or not hasattr(util, 'samedevice'):
+        raise util.Abort(_('hardlinks are not supported on this system'))
     src = hg.repository(
         cmdutil.remoteui(repo, opts),
         ui.expandpath(origin or 'default-relink', origin or 'default'))
@@ -45,7 +47,7 @@
         remotelock = src.lock()
         try:
             candidates = collect(src.store.path, ui)
-            targets = prune(candidates, repo.store.path, ui)
+            targets = prune(candidates, src.store.path, repo.store.path, ui)
             do_relink(src.store.path, repo.store.path, targets, ui)
         finally:
             remotelock.release()
@@ -68,16 +70,16 @@
     ui.status(_('collected %d candidate storage files\n') % len(candidates))
     return candidates
 
-def prune(candidates, dst, ui):
-    def linkfilter(dst, st):
+def prune(candidates, src, dst, ui):
+    def linkfilter(src, dst, st):
         try:
             ts = os.stat(dst)
         except OSError:
             # Destination doesn't have this file?
             return False
-        if st.st_ino == ts.st_ino:
+        if util.samefile(src, dst):
             return False
-        if st.st_dev != ts.st_dev:
+        if not util.samedevice(src, dst):
             # No point in continuing
             raise util.Abort(
                 _('source and destination are on different devices'))
@@ -87,8 +89,9 @@
 
     targets = []
     for fn, st in candidates:
+        srcpath = os.path.join(src, fn)
         tgt = os.path.join(dst, fn)
-        ts = linkfilter(tgt, st)
+        ts = linkfilter(srcpath, tgt, st)
         if not ts:
             ui.debug(_('not linkable: %s\n') % fn)
             continue
@@ -102,7 +105,7 @@
         bak = dst + '.bak'
         os.rename(dst, bak)
         try:
-            os.link(src, dst)
+            util.os_link(src, dst)
         except OSError:
             os.rename(bak, dst)
             raise
@@ -118,14 +121,17 @@
         pos += 1
         source = os.path.join(src, f)
         tgt = os.path.join(dst, f)
-        sfp = file(source)
-        dfp = file(tgt)
+        # Binary mode, so that read() works correctly, especially on Windows
+        sfp = file(source, 'rb')
+        dfp = file(tgt, 'rb')
         sin = sfp.read(CHUNKLEN)
         while sin:
             din = dfp.read(CHUNKLEN)
             if sin != din:
                 break
             sin = sfp.read(CHUNKLEN)
+        sfp.close()
+        dfp.close()
         if sin:
             ui.debug(_('not linkable: %s\n') % f)
             continue
--- a/mercurial/posix.py	Fri Jan 08 22:30:07 2010 +0100
+++ b/mercurial/posix.py	Fri Jan 08 18:48:39 2010 +0530
@@ -105,6 +105,18 @@
 def localpath(path):
     return path
 
+def samefile(fpath1, fpath2):
+    """Returns whether path1 and path2 refer to the same file. This is only
+    guaranteed to work for files, not directories."""
+    return os.path.samefile(fpath1, fpath2)
+
+def samedevice(fpath1, fpath2):
+    """Returns whether fpath1 and fpath2 are on the same device. This is only
+    guaranteed to work for files, not directories."""
+    st1 = os.lstat(fpath1)
+    st2 = os.lstat(fpath2)
+    return st1.st_dev == st2.st_dev
+
 if sys.platform == 'darwin':
     def realpath(path):
         '''
--- a/mercurial/win32.py	Fri Jan 08 22:30:07 2010 +0100
+++ b/mercurial/win32.py	Fri Jan 08 18:48:39 2010 +0530
@@ -37,7 +37,7 @@
     except NotImplementedError: # Another fake error win Win98
         raise OSError(errno.EINVAL, 'Hardlinking not supported')
 
-def nlinks(pathname):
+def _getfileinfo(pathname):
     """Return number of hardlinks for the given file."""
     try:
         fh = win32file.CreateFile(pathname,
@@ -45,10 +45,39 @@
             None, win32file.OPEN_EXISTING, 0, None)
         res = win32file.GetFileInformationByHandle(fh)
         fh.Close()
-        return res[7]
+        return res
     except pywintypes.error:
+        return None
+
+def nlinks(pathname):
+    """Return number of hardlinks for the given file."""
+    res = _getfileinfo(pathname)
+    if res is not None:
+        return res[7]
+    else:
         return os.lstat(pathname).st_nlink
 
+def samefile(fpath1, fpath2):
+    """Returns whether fpath1 and fpath2 refer to the same file. This is only
+    guaranteed to work for files, not directories."""
+    res1 = _getfileinfo(fpath1)
+    res2 = _getfileinfo(fpath2)
+    if res1 is not None and res2 is not None:
+        # Index 4 is the volume serial number, and 8 and 9 contain the file ID
+        return res1[4] == res2[4] and res1[8] == res2[8] and res1[9] == res2[9]
+    else:
+        return False
+
+def samedevice(fpath1, fpath2):
+    """Returns whether fpath1 and fpath2 are on the same device. This is only
+    guaranteed to work for files, not directories."""
+    res1 = _getfileinfo(fpath1)
+    res2 = _getfileinfo(fpath2)
+    if res1 is not None and res2 is not None:
+        return res1[4] == res2[4]
+    else:
+        return False
+
 def testpid(pid):
     '''return True if pid is still running or unable to
     determine, False otherwise'''
--- a/tests/test-relink	Fri Jan 08 22:30:07 2010 +0100
+++ b/tests/test-relink	Fri Jan 08 18:48:39 2010 +0530
@@ -3,10 +3,16 @@
 echo "[extensions]" >> $HGRCPATH
 echo "relink=" >> $HGRCPATH
 
+fix_path()
+{
+    tr '\\' /
+}
+
 cat > arelinked.py <<EOF
 import sys, os
+from mercurial import util
 path1, path2 = sys.argv[1:3]
-if os.stat(path1).st_ino == os.stat(path2).st_ino:
+if util.samefile(path1, path2):
     print '%s == %s' % (path1, path2)
 else:
     print '%s != %s' % (path1, path2)
@@ -23,6 +29,8 @@
 echo a >> a
 echo a >> b
 hg ci -Am changefiles
+# Test files are read in binary mode
+python -c "file('.hg/store/data/dummy.i', 'wb').write('a\r\nb\n')"
 cd ..
 
 echo '% clone and pull to break links'
@@ -33,9 +41,11 @@
 hg pull -q
 echo b >> b
 hg ci -m changeb
+python -c "file('.hg/store/data/dummy.i', 'wb').write('a\nb\r\n')"
 
 echo '% relink'
-hg relink --debug | sed 's:relinking.*store:relinking .hg/store:g'
+hg relink --debug | sed 's:relinking.*store:relinking .hg/store:g' \
+    | fix_path
 cd ..
 
 echo '% check hardlinks'
--- a/tests/test-relink.out	Fri Jan 08 22:30:07 2010 +0100
+++ b/tests/test-relink.out	Fri Jan 08 18:48:39 2010 +0530
@@ -12,12 +12,13 @@
 created new head
 % relink
 relinking .hg/store
-collected 4 candidate storage files
+collected 5 candidate storage files
 not linkable: 00changelog.i
 not linkable: 00manifest.i
 not linkable: data/b.i
-pruned down to 1 probably relinkable files
-relink: data/a.i 1/1  files (1e+02%)
+pruned down to 2 probably relinkable files
+relink: data/a.i 1/2  files (  50%)
+not linkable: data/dummy.i
 relinked 1 files (136 bytes reclaimed)
 % check hardlinks
 repo/.hg/store/data/a.i == clone/.hg/store/data/a.i