# HG changeset patch # User Siddharth Agarwal # Date 1262956719 -19800 # Node ID 750b7a4f01f6c80b342620c976f1f58d5d188987 # Parent 2bbb4c8eb27e872c3b8744fc00c72aa3d17c836a Add support for relinking on Windows. Test and minor code change by Patrick Mézard diff -r 2bbb4c8eb27e -r 750b7a4f01f6 hgext/relink.py --- 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 diff -r 2bbb4c8eb27e -r 750b7a4f01f6 mercurial/posix.py --- 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): ''' diff -r 2bbb4c8eb27e -r 750b7a4f01f6 mercurial/win32.py --- 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''' diff -r 2bbb4c8eb27e -r 750b7a4f01f6 tests/test-relink --- 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 <> 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' diff -r 2bbb4c8eb27e -r 750b7a4f01f6 tests/test-relink.out --- 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