changeset 10221:489b0caf21ed

Merge with crew-stable
author Patrick Mezard <pmezard@gmail.com>
date Sat, 09 Jan 2010 00:25:56 +0100
parents 843f6ee6d14b (current diff) 500d09be7ace (diff)
children 98f630e15d82
files hgext/relink.py mercurial/ui.py
diffstat 6 files changed, 141 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/hgext/relink.py	Thu Jan 07 16:06:36 2010 +0100
+++ b/hgext/relink.py	Sat Jan 09 00:25:56 2010 +0100
@@ -36,6 +36,8 @@
     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'))
@@ -47,7 +49,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()
@@ -70,16 +72,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'))
@@ -89,8 +91,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
@@ -104,7 +107,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
@@ -120,14 +123,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	Thu Jan 07 16:06:36 2010 +0100
+++ b/mercurial/posix.py	Sat Jan 09 00:25:56 2010 +0100
@@ -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/ui.py	Thu Jan 07 16:06:36 2010 +0100
+++ b/mercurial/ui.py	Sat Jan 09 00:25:56 2010 +0100
@@ -389,7 +389,7 @@
 
         if total:
             pct = 100.0 * pos / total
-            self.debug('%s:%s %s/%s%s (%4.2g%%)\n'
+            self.debug('%s:%s %s/%s%s (%4.2f%%)\n'
                      % (topic, item, pos, total, unit, pct))
         else:
             self.debug('%s:%s %s%s\n' % (topic, item, pos, unit))
--- a/mercurial/win32.py	Thu Jan 07 16:06:36 2010 +0100
+++ b/mercurial/win32.py	Sat Jan 09 00:25:56 2010 +0100
@@ -37,18 +37,48 @@
     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,
             win32file.GENERIC_READ, win32file.FILE_SHARE_READ,
             None, win32file.OPEN_EXISTING, 0, None)
-        res = win32file.GetFileInformationByHandle(fh)
-        fh.Close()
+        try:
+            return win32file.GetFileInformationByHandle(fh)
+        finally:
+            fh.Close()
+    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]
-    except pywintypes.error:
+    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'''
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-relink	Sat Jan 09 00:25:56 2010 +0100
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+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 util.samefile(path1, path2):
+    print '%s == %s' % (path1, path2)
+else:
+    print '%s != %s' % (path1, path2)
+EOF
+
+echo '% create source repository'
+hg init repo
+cd repo
+echo '[ui]' > .hg/hgrc
+echo 'username= A. Foo <a.foo@bar.com>' >> .hg/hgrc
+echo a > a
+echo b > b
+hg ci -Am addfile
+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'
+hg clone --pull -r0 repo clone
+cd clone
+echo '[ui]' >> .hg/hgrc
+echo 'username= A. Baz <a.baz@bar.com>' >> .hg/hgrc
+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' \
+    | fix_path
+cd ..
+
+echo '% check hardlinks'
+python arelinked.py repo/.hg/store/data/a.i clone/.hg/store/data/a.i
+python arelinked.py repo/.hg/store/data/b.i clone/.hg/store/data/b.i
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-relink.out	Sat Jan 09 00:25:56 2010 +0100
@@ -0,0 +1,25 @@
+% create source repository
+adding a
+adding b
+% clone and pull to break links
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 2 changes to 2 files
+updating to branch default
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+created new head
+% relink
+relinking .hg/store
+collected 5 candidate storage files
+not linkable: 00changelog.i
+not linkable: 00manifest.i
+not linkable: data/b.i
+pruned down to 2 probably relinkable files
+relink: data/a.i 1/2  files (50.00%)
+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
+repo/.hg/store/data/b.i != clone/.hg/store/data/b.i