# HG changeset patch # User Matt Mackall # Date 1174723047 18000 # Node ID cb6107f78b929aba1ca8866833a4ba85f5681ea5 # Parent 73c918c713003ff50b148e0ca381b22a3503e318# Parent 81402b2b294de1d7419cbe6662bd8080278545de Merge with crew diff --git a/contrib/hg-relink b/contrib/hg-relink --- a/contrib/hg-relink +++ b/contrib/hg-relink @@ -23,20 +23,13 @@ class Config: if not os.path.exists(os.path.join(d, '.hg')): raise ConfigError("%s: not a mercurial repository" % d) -try: - cfg = Config(sys.argv) -except ConfigError, inst: - print str(inst) - usage() - sys.exit(1) - def collect(src): seplen = len(os.path.sep) candidates = [] for dirpath, dirnames, filenames in os.walk(src): relpath = dirpath[len(src) + seplen:] for filename in filenames: - if not (filename.endswith('.i') or filename.endswith('.d')): + if not filename.endswith('.i'): continue st = os.stat(os.path.join(dirpath, filename)) candidates.append((os.path.join(relpath, filename), st)) @@ -44,25 +37,56 @@ def collect(src): return candidates def prune(candidates, dst): + def getdatafile(path): + if not path.endswith('.i'): + return None, None + df = path[:-1] + 'd' + try: + st = os.stat(df) + except OSError: + return None, None + return df, st + + def linkfilter(dst, st): + try: + ts = os.stat(dst) + except OSError: + # Destination doesn't have this file? + return False + if st.st_ino == ts.st_ino: + return False + if st.st_dev != ts.st_dev: + # No point in continuing + raise Exception('Source and destination are on different devices') + if st.st_size != ts.st_size: + # TODO: compare revlog heads + return False + return st + targets = [] for fn, st in candidates: tgt = os.path.join(dst, fn) - try: - ts = os.stat(tgt) - except OSError: - # Destination doesn't have this file? - continue - if st.st_ino == ts.st_ino: - continue - if st.st_dev != ts.st_dev: - raise Exception('Source and destination are on different devices') - if st.st_size != ts.st_size: + ts = linkfilter(tgt, st) + if not ts: continue targets.append((fn, ts.st_size)) + df, ts = getdatafile(tgt) + if df: + targets.append((fn[:-1] + 'd', ts.st_size)) return targets def relink(src, dst, files): + def relinkfile(src, dst): + bak = dst + '.bak' + os.rename(dst, bak) + try: + os.link(src, dst) + except OSError: + os.rename(bak, dst) + raise + os.remove(bak) + CHUNKLEN = 65536 relinked = 0 savedbytes = 0 @@ -81,21 +105,22 @@ def relink(src, dst, files): if sin: continue try: - os.rename(tgt, tgt + '.bak') - try: - os.link(source, tgt) - except OSError: - os.rename(tgt + '.bak', tgt) - raise + relinkfile(source, tgt) print 'Relinked %s' % f relinked += 1 savedbytes += sz - os.remove(tgt + '.bak') except OSError, inst: print '%s: %s' % (tgt, str(inst)) print 'Relinked %d files (%d bytes reclaimed)' % (relinked, savedbytes) +try: + cfg = Config(sys.argv) +except ConfigError, inst: + print str(inst) + usage() + sys.exit(1) + src = os.path.join(cfg.src, '.hg') dst = os.path.join(cfg.dst, '.hg') candidates = collect(src) diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -2255,7 +2255,8 @@ def revert(ui, repo, *pats, **opts): def handle(xlist, dobackup): xlist[0].append(abs) update[abs] = 1 - if dobackup and not opts['no_backup'] and os.path.exists(rel): + if (dobackup and not opts['no_backup'] and + (os.path.islink(rel) or os.path.exists(rel))): bakname = "%s.orig" % rel ui.note(_('saving current version of %s as %s\n') % (rel, bakname)) diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -102,11 +102,6 @@ class localrepository(repo.repository): self.filterpats = {} self.transhandle = None - self._link = lambda x: False - if util.checklink(self.root): - r = self.root # avoid circular reference in lambda - self._link = lambda x: util.is_link(os.path.join(r, x)) - self.dirstate = dirstate.dirstate(self.opener, self.ui, self.root) def url(self): @@ -505,6 +500,9 @@ class localrepository(repo.repository): def wfile(self, f, mode='r'): return self.wopener(f, mode) + def _link(self, f): + return os.path.islink(self.wjoin(f)) + def _filter(self, filter, filename, data): if filter not in self.filterpats: l = [] @@ -1052,10 +1050,11 @@ class localrepository(repo.repository): def copy(self, source, dest, wlock=None): p = self.wjoin(dest) - if not os.path.exists(p): + if not (os.path.exists(p) or os.path.islink(p)): self.ui.warn(_("%s does not exist!\n") % dest) - elif not os.path.isfile(p): - self.ui.warn(_("copy failed: %s is not a file\n") % dest) + elif not (os.path.isfile(p) or os.path.islink(p)): + self.ui.warn(_("copy failed: %s is not a file or a " + "symbolic link\n") % dest) else: if not wlock: wlock = self.wlock() diff --git a/mercurial/util.py b/mercurial/util.py --- a/mercurial/util.py +++ b/mercurial/util.py @@ -614,11 +614,18 @@ def unlink(f): def copyfile(src, dest): "copy a file, preserving mode" - try: - shutil.copyfile(src, dest) - shutil.copymode(src, dest) - except shutil.Error, inst: - raise Abort(str(inst)) + if os.path.islink(src): + try: + os.unlink(dest) + except: + pass + os.symlink(os.readlink(src), dest) + else: + try: + shutil.copyfile(src, dest) + shutil.copymode(src, dest) + except shutil.Error, inst: + raise Abort(str(inst)) def copyfiles(src, dst, hardlink=None): """Copy a directory tree using hardlinks if possible""" @@ -785,7 +792,7 @@ def checklink(path): def linkfunc(path, fallback): '''return an is_link() function with default to fallback''' if checklink(path): - return lambda x: is_link(os.path.join(path, x)) + return lambda x: os.path.islink(os.path.join(path, x)) return fallback # Platform specific variants @@ -961,10 +968,6 @@ else: else: os.chmod(f, s & 0666) - def is_link(f): - """check whether a file is a symlink""" - return (os.lstat(f).st_mode & 0120000 == 0120000) - def set_link(f, mode): """make a file a symbolic link/regular file @@ -972,7 +975,7 @@ else: if a link is changed to a file, its link data become its contents """ - m = is_link(f) + m = os.path.islink(f) if m == bool(mode): return diff --git a/tests/test-symlink-basic b/tests/test-symlink-basic new file mode 100755 --- /dev/null +++ b/tests/test-symlink-basic @@ -0,0 +1,45 @@ +#!/bin/sh + +cat >> readlink.py <', os.readlink(f) +EOF + +hg init a +cd a +ln -s nothing dangling +hg add dangling +hg commit -m 'add symlink' -d '0 0' + +hg tip -v +hg manifest --debug +echo '% rev 0:' +python ../readlink.py dangling + +rm dangling +ln -s void dangling +hg commit -m 'change symlink' +echo '% rev 1:' +python ../readlink.py dangling + +echo '% modifying link' +rm dangling +ln -s empty dangling +python ../readlink.py dangling + +echo '% reverting to rev 0:' +hg revert -r 0 -a +python ../readlink.py dangling + +echo '% backups:' +python ../readlink.py *.orig + +rm *.orig +hg up -C +echo '% copies' +hg cp -v dangling dangling2 +hg st -Cmard +python ../readlink.py dangling dangling2 diff --git a/tests/test-symlink-basic.out b/tests/test-symlink-basic.out new file mode 100644 --- /dev/null +++ b/tests/test-symlink-basic.out @@ -0,0 +1,28 @@ +changeset: 0:cabd88b706fc +tag: tip +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +files: dangling +description: +add symlink + + +2564acbe54bbbedfbf608479340b359f04597f80 644 dangling +% rev 0: +dangling -> nothing +% rev 1: +dangling -> void +% modifying link +dangling -> empty +% reverting to rev 0: +reverting dangling +dangling -> nothing +% backups: +dangling.orig -> empty +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +% copies +copying dangling to dangling2 +A dangling2 + dangling +dangling -> void +dangling2 -> void