hgext/relink.py
changeset 43076 2372284d9457
parent 40293 c303d65d2e34
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    16     error,
    16     error,
    17     hg,
    17     hg,
    18     registrar,
    18     registrar,
    19     util,
    19     util,
    20 )
    20 )
    21 from mercurial.utils import (
    21 from mercurial.utils import stringutil
    22     stringutil,
       
    23 )
       
    24 
    22 
    25 cmdtable = {}
    23 cmdtable = {}
    26 command = registrar.command(cmdtable)
    24 command = registrar.command(cmdtable)
    27 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
    25 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
    28 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    26 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    29 # be specifying the version(s) of Mercurial they are tested with, or
    27 # be specifying the version(s) of Mercurial they are tested with, or
    30 # leave the attribute unspecified.
    28 # leave the attribute unspecified.
    31 testedwith = 'ships-with-hg-core'
    29 testedwith = 'ships-with-hg-core'
    32 
    30 
       
    31 
    33 @command('relink', [], _('[ORIGIN]'), helpcategory=command.CATEGORY_MAINTENANCE)
    32 @command('relink', [], _('[ORIGIN]'), helpcategory=command.CATEGORY_MAINTENANCE)
    34 def relink(ui, repo, origin=None, **opts):
    33 def relink(ui, repo, origin=None, **opts):
    35     """recreate hardlinks between two repositories
    34     """recreate hardlinks between two repositories
    36 
    35 
    37     When repositories are cloned locally, their data files will be
    36     When repositories are cloned locally, their data files will be
    54 
    53 
    55     Do not attempt any read operations on this repository while the
    54     Do not attempt any read operations on this repository while the
    56     command is running. (Both repositories will be locked against
    55     command is running. (Both repositories will be locked against
    57     writes.)
    56     writes.)
    58     """
    57     """
    59     if (not util.safehasattr(util, 'samefile') or
    58     if not util.safehasattr(util, 'samefile') or not util.safehasattr(
    60         not util.safehasattr(util, 'samedevice')):
    59         util, 'samedevice'
       
    60     ):
    61         raise error.Abort(_('hardlinks are not supported on this system'))
    61         raise error.Abort(_('hardlinks are not supported on this system'))
    62     src = hg.repository(repo.baseui, ui.expandpath(origin or 'default-relink',
    62     src = hg.repository(
    63                                           origin or 'default'))
    63         repo.baseui,
       
    64         ui.expandpath(origin or 'default-relink', origin or 'default'),
       
    65     )
    64     ui.status(_('relinking %s to %s\n') % (src.store.path, repo.store.path))
    66     ui.status(_('relinking %s to %s\n') % (src.store.path, repo.store.path))
    65     if repo.root == src.root:
    67     if repo.root == src.root:
    66         ui.status(_('there is nothing to relink\n'))
    68         ui.status(_('there is nothing to relink\n'))
    67         return
    69         return
    68 
    70 
    72 
    74 
    73     with repo.lock(), src.lock():
    75     with repo.lock(), src.lock():
    74         candidates = sorted(collect(src, ui))
    76         candidates = sorted(collect(src, ui))
    75         targets = prune(candidates, src.store.path, repo.store.path, ui)
    77         targets = prune(candidates, src.store.path, repo.store.path, ui)
    76         do_relink(src.store.path, repo.store.path, targets, ui)
    78         do_relink(src.store.path, repo.store.path, targets, ui)
       
    79 
    77 
    80 
    78 def collect(src, ui):
    81 def collect(src, ui):
    79     seplen = len(os.path.sep)
    82     seplen = len(os.path.sep)
    80     candidates = []
    83     candidates = []
    81     live = len(src['tip'].manifest())
    84     live = len(src['tip'].manifest())
    87     # mozilla-central as of 2010-06-10 had a ratio of just over 7:5.
    90     # mozilla-central as of 2010-06-10 had a ratio of just over 7:5.
    88     total = live * 3 // 2
    91     total = live * 3 // 2
    89     src = src.store.path
    92     src = src.store.path
    90     progress = ui.makeprogress(_('collecting'), unit=_('files'), total=total)
    93     progress = ui.makeprogress(_('collecting'), unit=_('files'), total=total)
    91     pos = 0
    94     pos = 0
    92     ui.status(_("tip has %d files, estimated total number of files: %d\n")
    95     ui.status(
    93               % (live, total))
    96         _("tip has %d files, estimated total number of files: %d\n")
       
    97         % (live, total)
       
    98     )
    94     for dirpath, dirnames, filenames in os.walk(src):
    99     for dirpath, dirnames, filenames in os.walk(src):
    95         dirnames.sort()
   100         dirnames.sort()
    96         relpath = dirpath[len(src) + seplen:]
   101         relpath = dirpath[len(src) + seplen :]
    97         for filename in sorted(filenames):
   102         for filename in sorted(filenames):
    98             if filename[-2:] not in ('.d', '.i'):
   103             if filename[-2:] not in ('.d', '.i'):
    99                 continue
   104                 continue
   100             st = os.stat(os.path.join(dirpath, filename))
   105             st = os.stat(os.path.join(dirpath, filename))
   101             if not stat.S_ISREG(st.st_mode):
   106             if not stat.S_ISREG(st.st_mode):
   105             progress.update(pos, item=filename)
   110             progress.update(pos, item=filename)
   106 
   111 
   107     progress.complete()
   112     progress.complete()
   108     ui.status(_('collected %d candidate storage files\n') % len(candidates))
   113     ui.status(_('collected %d candidate storage files\n') % len(candidates))
   109     return candidates
   114     return candidates
       
   115 
   110 
   116 
   111 def prune(candidates, src, dst, ui):
   117 def prune(candidates, src, dst, ui):
   112     def linkfilter(src, dst, st):
   118     def linkfilter(src, dst, st):
   113         try:
   119         try:
   114             ts = os.stat(dst)
   120             ts = os.stat(dst)
   118         if util.samefile(src, dst):
   124         if util.samefile(src, dst):
   119             return False
   125             return False
   120         if not util.samedevice(src, dst):
   126         if not util.samedevice(src, dst):
   121             # No point in continuing
   127             # No point in continuing
   122             raise error.Abort(
   128             raise error.Abort(
   123                 _('source and destination are on different devices'))
   129                 _('source and destination are on different devices')
       
   130             )
   124         if st.st_size != ts.st_size:
   131         if st.st_size != ts.st_size:
   125             return False
   132             return False
   126         return st
   133         return st
   127 
   134 
   128     targets = []
   135     targets = []
   129     progress = ui.makeprogress(_('pruning'), unit=_('files'),
   136     progress = ui.makeprogress(
   130                                total=len(candidates))
   137         _('pruning'), unit=_('files'), total=len(candidates)
       
   138     )
   131     pos = 0
   139     pos = 0
   132     for fn, st in candidates:
   140     for fn, st in candidates:
   133         pos += 1
   141         pos += 1
   134         srcpath = os.path.join(src, fn)
   142         srcpath = os.path.join(src, fn)
   135         tgt = os.path.join(dst, fn)
   143         tgt = os.path.join(dst, fn)
   142 
   150 
   143     progress.complete()
   151     progress.complete()
   144     ui.status(_('pruned down to %d probably relinkable files\n') % len(targets))
   152     ui.status(_('pruned down to %d probably relinkable files\n') % len(targets))
   145     return targets
   153     return targets
   146 
   154 
       
   155 
   147 def do_relink(src, dst, files, ui):
   156 def do_relink(src, dst, files, ui):
   148     def relinkfile(src, dst):
   157     def relinkfile(src, dst):
   149         bak = dst + '.bak'
   158         bak = dst + '.bak'
   150         os.rename(dst, bak)
   159         os.rename(dst, bak)
   151         try:
   160         try:
   157 
   166 
   158     CHUNKLEN = 65536
   167     CHUNKLEN = 65536
   159     relinked = 0
   168     relinked = 0
   160     savedbytes = 0
   169     savedbytes = 0
   161 
   170 
   162     progress = ui.makeprogress(_('relinking'), unit=_('files'),
   171     progress = ui.makeprogress(
   163                                total=len(files))
   172         _('relinking'), unit=_('files'), total=len(files)
       
   173     )
   164     pos = 0
   174     pos = 0
   165     for f, sz in files:
   175     for f, sz in files:
   166         pos += 1
   176         pos += 1
   167         source = os.path.join(src, f)
   177         source = os.path.join(src, f)
   168         tgt = os.path.join(dst, f)
   178         tgt = os.path.join(dst, f)
   188         except OSError as inst:
   198         except OSError as inst:
   189             ui.warn('%s: %s\n' % (tgt, stringutil.forcebytestr(inst)))
   199             ui.warn('%s: %s\n' % (tgt, stringutil.forcebytestr(inst)))
   190 
   200 
   191     progress.complete()
   201     progress.complete()
   192 
   202 
   193     ui.status(_('relinked %d files (%s reclaimed)\n') %
   203     ui.status(
   194               (relinked, util.bytecount(savedbytes)))
   204         _('relinked %d files (%s reclaimed)\n')
       
   205         % (relinked, util.bytecount(savedbytes))
       
   206     )