hgext/relink.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43085 eef9a2d67051
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    24 command = registrar.command(cmdtable)
    24 command = registrar.command(cmdtable)
    25 # 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
    26 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    26 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    27 # 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
    28 # leave the attribute unspecified.
    28 # leave the attribute unspecified.
    29 testedwith = 'ships-with-hg-core'
    29 testedwith = b'ships-with-hg-core'
    30 
    30 
    31 
    31 
    32 @command('relink', [], _('[ORIGIN]'), helpcategory=command.CATEGORY_MAINTENANCE)
    32 @command(
       
    33     b'relink', [], _(b'[ORIGIN]'), helpcategory=command.CATEGORY_MAINTENANCE
       
    34 )
    33 def relink(ui, repo, origin=None, **opts):
    35 def relink(ui, repo, origin=None, **opts):
    34     """recreate hardlinks between two repositories
    36     """recreate hardlinks between two repositories
    35 
    37 
    36     When repositories are cloned locally, their data files will be
    38     When repositories are cloned locally, their data files will be
    37     hardlinked so that they only use the space of a single repository.
    39     hardlinked so that they only use the space of a single repository.
    53 
    55 
    54     Do not attempt any read operations on this repository while the
    56     Do not attempt any read operations on this repository while the
    55     command is running. (Both repositories will be locked against
    57     command is running. (Both repositories will be locked against
    56     writes.)
    58     writes.)
    57     """
    59     """
    58     if not util.safehasattr(util, 'samefile') or not util.safehasattr(
    60     if not util.safehasattr(util, b'samefile') or not util.safehasattr(
    59         util, 'samedevice'
    61         util, b'samedevice'
    60     ):
    62     ):
    61         raise error.Abort(_('hardlinks are not supported on this system'))
    63         raise error.Abort(_(b'hardlinks are not supported on this system'))
    62     src = hg.repository(
    64     src = hg.repository(
    63         repo.baseui,
    65         repo.baseui,
    64         ui.expandpath(origin or 'default-relink', origin or 'default'),
    66         ui.expandpath(origin or b'default-relink', origin or b'default'),
    65     )
    67     )
    66     ui.status(_('relinking %s to %s\n') % (src.store.path, repo.store.path))
    68     ui.status(_(b'relinking %s to %s\n') % (src.store.path, repo.store.path))
    67     if repo.root == src.root:
    69     if repo.root == src.root:
    68         ui.status(_('there is nothing to relink\n'))
    70         ui.status(_(b'there is nothing to relink\n'))
    69         return
    71         return
    70 
    72 
    71     if not util.samedevice(src.store.path, repo.store.path):
    73     if not util.samedevice(src.store.path, repo.store.path):
    72         # No point in continuing
    74         # No point in continuing
    73         raise error.Abort(_('source and destination are on different devices'))
    75         raise error.Abort(_(b'source and destination are on different devices'))
    74 
    76 
    75     with repo.lock(), src.lock():
    77     with repo.lock(), src.lock():
    76         candidates = sorted(collect(src, ui))
    78         candidates = sorted(collect(src, ui))
    77         targets = prune(candidates, src.store.path, repo.store.path, ui)
    79         targets = prune(candidates, src.store.path, repo.store.path, ui)
    78         do_relink(src.store.path, repo.store.path, targets, ui)
    80         do_relink(src.store.path, repo.store.path, targets, ui)
    79 
    81 
    80 
    82 
    81 def collect(src, ui):
    83 def collect(src, ui):
    82     seplen = len(os.path.sep)
    84     seplen = len(os.path.sep)
    83     candidates = []
    85     candidates = []
    84     live = len(src['tip'].manifest())
    86     live = len(src[b'tip'].manifest())
    85     # Your average repository has some files which were deleted before
    87     # Your average repository has some files which were deleted before
    86     # the tip revision. We account for that by assuming that there are
    88     # the tip revision. We account for that by assuming that there are
    87     # 3 tracked files for every 2 live files as of the tip version of
    89     # 3 tracked files for every 2 live files as of the tip version of
    88     # the repository.
    90     # the repository.
    89     #
    91     #
    90     # mozilla-central as of 2010-06-10 had a ratio of just over 7:5.
    92     # mozilla-central as of 2010-06-10 had a ratio of just over 7:5.
    91     total = live * 3 // 2
    93     total = live * 3 // 2
    92     src = src.store.path
    94     src = src.store.path
    93     progress = ui.makeprogress(_('collecting'), unit=_('files'), total=total)
    95     progress = ui.makeprogress(_(b'collecting'), unit=_(b'files'), total=total)
    94     pos = 0
    96     pos = 0
    95     ui.status(
    97     ui.status(
    96         _("tip has %d files, estimated total number of files: %d\n")
    98         _(b"tip has %d files, estimated total number of files: %d\n")
    97         % (live, total)
    99         % (live, total)
    98     )
   100     )
    99     for dirpath, dirnames, filenames in os.walk(src):
   101     for dirpath, dirnames, filenames in os.walk(src):
   100         dirnames.sort()
   102         dirnames.sort()
   101         relpath = dirpath[len(src) + seplen :]
   103         relpath = dirpath[len(src) + seplen :]
   102         for filename in sorted(filenames):
   104         for filename in sorted(filenames):
   103             if filename[-2:] not in ('.d', '.i'):
   105             if filename[-2:] not in (b'.d', b'.i'):
   104                 continue
   106                 continue
   105             st = os.stat(os.path.join(dirpath, filename))
   107             st = os.stat(os.path.join(dirpath, filename))
   106             if not stat.S_ISREG(st.st_mode):
   108             if not stat.S_ISREG(st.st_mode):
   107                 continue
   109                 continue
   108             pos += 1
   110             pos += 1
   109             candidates.append((os.path.join(relpath, filename), st))
   111             candidates.append((os.path.join(relpath, filename), st))
   110             progress.update(pos, item=filename)
   112             progress.update(pos, item=filename)
   111 
   113 
   112     progress.complete()
   114     progress.complete()
   113     ui.status(_('collected %d candidate storage files\n') % len(candidates))
   115     ui.status(_(b'collected %d candidate storage files\n') % len(candidates))
   114     return candidates
   116     return candidates
   115 
   117 
   116 
   118 
   117 def prune(candidates, src, dst, ui):
   119 def prune(candidates, src, dst, ui):
   118     def linkfilter(src, dst, st):
   120     def linkfilter(src, dst, st):
   124         if util.samefile(src, dst):
   126         if util.samefile(src, dst):
   125             return False
   127             return False
   126         if not util.samedevice(src, dst):
   128         if not util.samedevice(src, dst):
   127             # No point in continuing
   129             # No point in continuing
   128             raise error.Abort(
   130             raise error.Abort(
   129                 _('source and destination are on different devices')
   131                 _(b'source and destination are on different devices')
   130             )
   132             )
   131         if st.st_size != ts.st_size:
   133         if st.st_size != ts.st_size:
   132             return False
   134             return False
   133         return st
   135         return st
   134 
   136 
   135     targets = []
   137     targets = []
   136     progress = ui.makeprogress(
   138     progress = ui.makeprogress(
   137         _('pruning'), unit=_('files'), total=len(candidates)
   139         _(b'pruning'), unit=_(b'files'), total=len(candidates)
   138     )
   140     )
   139     pos = 0
   141     pos = 0
   140     for fn, st in candidates:
   142     for fn, st in candidates:
   141         pos += 1
   143         pos += 1
   142         srcpath = os.path.join(src, fn)
   144         srcpath = os.path.join(src, fn)
   143         tgt = os.path.join(dst, fn)
   145         tgt = os.path.join(dst, fn)
   144         ts = linkfilter(srcpath, tgt, st)
   146         ts = linkfilter(srcpath, tgt, st)
   145         if not ts:
   147         if not ts:
   146             ui.debug('not linkable: %s\n' % fn)
   148             ui.debug(b'not linkable: %s\n' % fn)
   147             continue
   149             continue
   148         targets.append((fn, ts.st_size))
   150         targets.append((fn, ts.st_size))
   149         progress.update(pos, item=fn)
   151         progress.update(pos, item=fn)
   150 
   152 
   151     progress.complete()
   153     progress.complete()
   152     ui.status(_('pruned down to %d probably relinkable files\n') % len(targets))
   154     ui.status(
       
   155         _(b'pruned down to %d probably relinkable files\n') % len(targets)
       
   156     )
   153     return targets
   157     return targets
   154 
   158 
   155 
   159 
   156 def do_relink(src, dst, files, ui):
   160 def do_relink(src, dst, files, ui):
   157     def relinkfile(src, dst):
   161     def relinkfile(src, dst):
   158         bak = dst + '.bak'
   162         bak = dst + b'.bak'
   159         os.rename(dst, bak)
   163         os.rename(dst, bak)
   160         try:
   164         try:
   161             util.oslink(src, dst)
   165             util.oslink(src, dst)
   162         except OSError:
   166         except OSError:
   163             os.rename(bak, dst)
   167             os.rename(bak, dst)
   167     CHUNKLEN = 65536
   171     CHUNKLEN = 65536
   168     relinked = 0
   172     relinked = 0
   169     savedbytes = 0
   173     savedbytes = 0
   170 
   174 
   171     progress = ui.makeprogress(
   175     progress = ui.makeprogress(
   172         _('relinking'), unit=_('files'), total=len(files)
   176         _(b'relinking'), unit=_(b'files'), total=len(files)
   173     )
   177     )
   174     pos = 0
   178     pos = 0
   175     for f, sz in files:
   179     for f, sz in files:
   176         pos += 1
   180         pos += 1
   177         source = os.path.join(src, f)
   181         source = os.path.join(src, f)
   178         tgt = os.path.join(dst, f)
   182         tgt = os.path.join(dst, f)
   179         # Binary mode, so that read() works correctly, especially on Windows
   183         # Binary mode, so that read() works correctly, especially on Windows
   180         sfp = open(source, 'rb')
   184         sfp = open(source, b'rb')
   181         dfp = open(tgt, 'rb')
   185         dfp = open(tgt, b'rb')
   182         sin = sfp.read(CHUNKLEN)
   186         sin = sfp.read(CHUNKLEN)
   183         while sin:
   187         while sin:
   184             din = dfp.read(CHUNKLEN)
   188             din = dfp.read(CHUNKLEN)
   185             if sin != din:
   189             if sin != din:
   186                 break
   190                 break
   187             sin = sfp.read(CHUNKLEN)
   191             sin = sfp.read(CHUNKLEN)
   188         sfp.close()
   192         sfp.close()
   189         dfp.close()
   193         dfp.close()
   190         if sin:
   194         if sin:
   191             ui.debug('not linkable: %s\n' % f)
   195             ui.debug(b'not linkable: %s\n' % f)
   192             continue
   196             continue
   193         try:
   197         try:
   194             relinkfile(source, tgt)
   198             relinkfile(source, tgt)
   195             progress.update(pos, item=f)
   199             progress.update(pos, item=f)
   196             relinked += 1
   200             relinked += 1
   197             savedbytes += sz
   201             savedbytes += sz
   198         except OSError as inst:
   202         except OSError as inst:
   199             ui.warn('%s: %s\n' % (tgt, stringutil.forcebytestr(inst)))
   203             ui.warn(b'%s: %s\n' % (tgt, stringutil.forcebytestr(inst)))
   200 
   204 
   201     progress.complete()
   205     progress.complete()
   202 
   206 
   203     ui.status(
   207     ui.status(
   204         _('relinked %d files (%s reclaimed)\n')
   208         _(b'relinked %d files (%s reclaimed)\n')
   205         % (relinked, util.bytecount(savedbytes))
   209         % (relinked, util.bytecount(savedbytes))
   206     )
   210     )