--- 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