Add support for relinking on Windows.
Test and minor code change by Patrick Mézard <pmezard@gmail.com>
--- 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