eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/relink.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 # Mercurial extension to provide 'hg relink' command
       
     2 #
       
     3 # Copyright (C) 2007 Brendan Cully <brendan@kublai.com>
       
     4 #
       
     5 # This software may be used and distributed according to the terms of the
       
     6 # GNU General Public License version 2 or any later version.
       
     7 
       
     8 """recreates hardlinks between repository clones"""
       
     9 
       
    10 from mercurial import hg, util
       
    11 from mercurial.i18n import _
       
    12 import os, stat
       
    13 
       
    14 def relink(ui, repo, origin=None, **opts):
       
    15     """recreate hardlinks between two repositories
       
    16 
       
    17     When repositories are cloned locally, their data files will be
       
    18     hardlinked so that they only use the space of a single repository.
       
    19 
       
    20     Unfortunately, subsequent pulls into either repository will break
       
    21     hardlinks for any files touched by the new changesets, even if
       
    22     both repositories end up pulling the same changes.
       
    23 
       
    24     Similarly, passing --rev to "hg clone" will fail to use any
       
    25     hardlinks, falling back to a complete copy of the source
       
    26     repository.
       
    27 
       
    28     This command lets you recreate those hardlinks and reclaim that
       
    29     wasted space.
       
    30 
       
    31     This repository will be relinked to share space with ORIGIN, which
       
    32     must be on the same local disk. If ORIGIN is omitted, looks for
       
    33     "default-relink", then "default", in [paths].
       
    34 
       
    35     Do not attempt any read operations on this repository while the
       
    36     command is running. (Both repositories will be locked against
       
    37     writes.)
       
    38     """
       
    39     if not hasattr(util, 'samefile') or not hasattr(util, 'samedevice'):
       
    40         raise util.Abort(_('hardlinks are not supported on this system'))
       
    41     src = hg.repository(
       
    42         hg.remoteui(repo, opts),
       
    43         ui.expandpath(origin or 'default-relink', origin or 'default'))
       
    44     if not src.local():
       
    45         raise util.Abort('must specify local origin repository')
       
    46     ui.status(_('relinking %s to %s\n') % (src.store.path, repo.store.path))
       
    47     locallock = repo.lock()
       
    48     try:
       
    49         remotelock = src.lock()
       
    50         try:
       
    51             candidates = sorted(collect(src, ui))
       
    52             targets = prune(candidates, src.store.path, repo.store.path, ui)
       
    53             do_relink(src.store.path, repo.store.path, targets, ui)
       
    54         finally:
       
    55             remotelock.release()
       
    56     finally:
       
    57         locallock.release()
       
    58 
       
    59 def collect(src, ui):
       
    60     seplen = len(os.path.sep)
       
    61     candidates = []
       
    62     live = len(src['tip'].manifest())
       
    63     # Your average repository has some files which were deleted before
       
    64     # the tip revision. We account for that by assuming that there are
       
    65     # 3 tracked files for every 2 live files as of the tip version of
       
    66     # the repository.
       
    67     #
       
    68     # mozilla-central as of 2010-06-10 had a ratio of just over 7:5.
       
    69     total = live * 3 // 2
       
    70     src = src.store.path
       
    71     pos = 0
       
    72     ui.status(_("tip has %d files, estimated total number of files: %s\n")
       
    73               % (live, total))
       
    74     for dirpath, dirnames, filenames in os.walk(src):
       
    75         dirnames.sort()
       
    76         relpath = dirpath[len(src) + seplen:]
       
    77         for filename in sorted(filenames):
       
    78             if not filename[-2:] in ('.d', '.i'):
       
    79                 continue
       
    80             st = os.stat(os.path.join(dirpath, filename))
       
    81             if not stat.S_ISREG(st.st_mode):
       
    82                 continue
       
    83             pos += 1
       
    84             candidates.append((os.path.join(relpath, filename), st))
       
    85             ui.progress(_('collecting'), pos, filename, _('files'), total)
       
    86 
       
    87     ui.progress(_('collecting'), None)
       
    88     ui.status(_('collected %d candidate storage files\n') % len(candidates))
       
    89     return candidates
       
    90 
       
    91 def prune(candidates, src, dst, ui):
       
    92     def linkfilter(src, dst, st):
       
    93         try:
       
    94             ts = os.stat(dst)
       
    95         except OSError:
       
    96             # Destination doesn't have this file?
       
    97             return False
       
    98         if util.samefile(src, dst):
       
    99             return False
       
   100         if not util.samedevice(src, dst):
       
   101             # No point in continuing
       
   102             raise util.Abort(
       
   103                 _('source and destination are on different devices'))
       
   104         if st.st_size != ts.st_size:
       
   105             return False
       
   106         return st
       
   107 
       
   108     targets = []
       
   109     total = len(candidates)
       
   110     pos = 0
       
   111     for fn, st in candidates:
       
   112         pos += 1
       
   113         srcpath = os.path.join(src, fn)
       
   114         tgt = os.path.join(dst, fn)
       
   115         ts = linkfilter(srcpath, tgt, st)
       
   116         if not ts:
       
   117             ui.debug(_('not linkable: %s\n') % fn)
       
   118             continue
       
   119         targets.append((fn, ts.st_size))
       
   120         ui.progress(_('pruning'), pos, fn, _('files'), total)
       
   121 
       
   122     ui.progress(_('pruning'), None)
       
   123     ui.status(_('pruned down to %d probably relinkable files\n') % len(targets))
       
   124     return targets
       
   125 
       
   126 def do_relink(src, dst, files, ui):
       
   127     def relinkfile(src, dst):
       
   128         bak = dst + '.bak'
       
   129         os.rename(dst, bak)
       
   130         try:
       
   131             util.os_link(src, dst)
       
   132         except OSError:
       
   133             os.rename(bak, dst)
       
   134             raise
       
   135         os.remove(bak)
       
   136 
       
   137     CHUNKLEN = 65536
       
   138     relinked = 0
       
   139     savedbytes = 0
       
   140 
       
   141     pos = 0
       
   142     total = len(files)
       
   143     for f, sz in files:
       
   144         pos += 1
       
   145         source = os.path.join(src, f)
       
   146         tgt = os.path.join(dst, f)
       
   147         # Binary mode, so that read() works correctly, especially on Windows
       
   148         sfp = file(source, 'rb')
       
   149         dfp = file(tgt, 'rb')
       
   150         sin = sfp.read(CHUNKLEN)
       
   151         while sin:
       
   152             din = dfp.read(CHUNKLEN)
       
   153             if sin != din:
       
   154                 break
       
   155             sin = sfp.read(CHUNKLEN)
       
   156         sfp.close()
       
   157         dfp.close()
       
   158         if sin:
       
   159             ui.debug(_('not linkable: %s\n') % f)
       
   160             continue
       
   161         try:
       
   162             relinkfile(source, tgt)
       
   163             ui.progress(_('relinking'), pos, f, _('files'), total)
       
   164             relinked += 1
       
   165             savedbytes += sz
       
   166         except OSError, inst:
       
   167             ui.warn('%s: %s\n' % (tgt, str(inst)))
       
   168 
       
   169     ui.progress(_('relinking'), None)
       
   170 
       
   171     ui.status(_('relinked %d files (%d bytes reclaimed)\n') %
       
   172               (relinked, savedbytes))
       
   173 
       
   174 cmdtable = {
       
   175     'relink': (
       
   176         relink,
       
   177         [],
       
   178         _('[ORIGIN]')
       
   179     )
       
   180 }