mercurial/archival.py
changeset 43076 2372284d9457
parent 42940 c04e0836f039
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    14 import time
    14 import time
    15 import zipfile
    15 import zipfile
    16 import zlib
    16 import zlib
    17 
    17 
    18 from .i18n import _
    18 from .i18n import _
    19 from .node import (
    19 from .node import nullrev
    20     nullrev,
       
    21 )
       
    22 
    20 
    23 from . import (
    21 from . import (
    24     error,
    22     error,
    25     formatter,
    23     formatter,
    26     match as matchmod,
    24     match as matchmod,
    27     pycompat,
    25     pycompat,
    28     scmutil,
    26     scmutil,
    29     util,
    27     util,
    30     vfs as vfsmod,
    28     vfs as vfsmod,
    31 )
    29 )
       
    30 
    32 stringio = util.stringio
    31 stringio = util.stringio
    33 
    32 
    34 # from unzip source code:
    33 # from unzip source code:
    35 _UNX_IFREG = 0x8000
    34 _UNX_IFREG = 0x8000
    36 _UNX_IFLNK = 0xa000
    35 _UNX_IFLNK = 0xA000
       
    36 
    37 
    37 
    38 def tidyprefix(dest, kind, prefix):
    38 def tidyprefix(dest, kind, prefix):
    39     '''choose prefix to use for names in archive.  make sure prefix is
    39     '''choose prefix to use for names in archive.  make sure prefix is
    40     safe for consumers.'''
    40     safe for consumers.'''
    41 
    41 
    46             raise ValueError('dest must be string if no prefix')
    46             raise ValueError('dest must be string if no prefix')
    47         prefix = os.path.basename(dest)
    47         prefix = os.path.basename(dest)
    48         lower = prefix.lower()
    48         lower = prefix.lower()
    49         for sfx in exts.get(kind, []):
    49         for sfx in exts.get(kind, []):
    50             if lower.endswith(sfx):
    50             if lower.endswith(sfx):
    51                 prefix = prefix[:-len(sfx)]
    51                 prefix = prefix[: -len(sfx)]
    52                 break
    52                 break
    53     lpfx = os.path.normpath(util.localpath(prefix))
    53     lpfx = os.path.normpath(util.localpath(prefix))
    54     prefix = util.pconvert(lpfx)
    54     prefix = util.pconvert(lpfx)
    55     if not prefix.endswith('/'):
    55     if not prefix.endswith('/'):
    56         prefix += '/'
    56         prefix += '/'
    60         prefix = prefix[2:]
    60         prefix = prefix[2:]
    61     if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix:
    61     if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix:
    62         raise error.Abort(_('archive prefix contains illegal components'))
    62         raise error.Abort(_('archive prefix contains illegal components'))
    63     return prefix
    63     return prefix
    64 
    64 
       
    65 
    65 exts = {
    66 exts = {
    66     'tar': ['.tar'],
    67     'tar': ['.tar'],
    67     'tbz2': ['.tbz2', '.tar.bz2'],
    68     'tbz2': ['.tbz2', '.tar.bz2'],
    68     'tgz': ['.tgz', '.tar.gz'],
    69     'tgz': ['.tgz', '.tar.gz'],
    69     'zip': ['.zip'],
    70     'zip': ['.zip'],
    70     'txz': ['.txz', '.tar.xz']
    71     'txz': ['.txz', '.tar.xz'],
    71     }
    72 }
       
    73 
    72 
    74 
    73 def guesskind(dest):
    75 def guesskind(dest):
    74     for kind, extensions in exts.iteritems():
    76     for kind, extensions in exts.iteritems():
    75         if any(dest.endswith(ext) for ext in extensions):
    77         if any(dest.endswith(ext) for ext in extensions):
    76             return kind
    78             return kind
    77     return None
    79     return None
    78 
    80 
       
    81 
    79 def _rootctx(repo):
    82 def _rootctx(repo):
    80     # repo[0] may be hidden
    83     # repo[0] may be hidden
    81     for rev in repo:
    84     for rev in repo:
    82         return repo[rev]
    85         return repo[rev]
    83     return repo[nullrev]
    86     return repo[nullrev]
       
    87 
    84 
    88 
    85 # {tags} on ctx includes local tags and 'tip', with no current way to limit
    89 # {tags} on ctx includes local tags and 'tip', with no current way to limit
    86 # that to global tags.  Therefore, use {latesttag} as a substitute when
    90 # that to global tags.  Therefore, use {latesttag} as a substitute when
    87 # the distance is 0, since that will be the list of global tags on ctx.
    91 # the distance is 0, since that will be the list of global tags on ctx.
    88 _defaultmetatemplate = br'''
    92 _defaultmetatemplate = br'''
    92 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"),
    96 {ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"),
    93       separate("\n",
    97       separate("\n",
    94                join(latesttag % "latesttag: {tag}", "\n"),
    98                join(latesttag % "latesttag: {tag}", "\n"),
    95                "latesttagdistance: {latesttagdistance}",
    99                "latesttagdistance: {latesttagdistance}",
    96                "changessincelatesttag: {changessincelatesttag}"))}
   100                "changessincelatesttag: {changessincelatesttag}"))}
    97 '''[1:]  # drop leading '\n'
   101 '''[
       
   102     1:
       
   103 ]  # drop leading '\n'
       
   104 
    98 
   105 
    99 def buildmetadata(ctx):
   106 def buildmetadata(ctx):
   100     '''build content of .hg_archival.txt'''
   107     '''build content of .hg_archival.txt'''
   101     repo = ctx.repo()
   108     repo = ctx.repo()
   102 
   109 
   103     opts = {
   110     opts = {
   104         'template': repo.ui.config('experimental', 'archivemetatemplate',
   111         'template': repo.ui.config(
   105                                    _defaultmetatemplate)
   112             'experimental', 'archivemetatemplate', _defaultmetatemplate
       
   113         )
   106     }
   114     }
   107 
   115 
   108     out = util.stringio()
   116     out = util.stringio()
   109 
   117 
   110     fm = formatter.formatter(repo.ui, out, 'archive', opts)
   118     fm = formatter.formatter(repo.ui, out, 'archive', opts)
   119         fm.data(dirty=dirty)
   127         fm.data(dirty=dirty)
   120     fm.end()
   128     fm.end()
   121 
   129 
   122     return out.getvalue()
   130     return out.getvalue()
   123 
   131 
       
   132 
   124 class tarit(object):
   133 class tarit(object):
   125     '''write archive to tar file or stream.  can write uncompressed,
   134     '''write archive to tar file or stream.  can write uncompressed,
   126     or compress with gzip or bzip2.'''
   135     or compress with gzip or bzip2.'''
   127 
   136 
   128     class GzipFileWithTime(gzip.GzipFile):
   137     class GzipFileWithTime(gzip.GzipFile):
   129 
       
   130         def __init__(self, *args, **kw):
   138         def __init__(self, *args, **kw):
   131             timestamp = None
   139             timestamp = None
   132             if r'timestamp' in kw:
   140             if r'timestamp' in kw:
   133                 timestamp = kw.pop(r'timestamp')
   141                 timestamp = kw.pop(r'timestamp')
   134             if timestamp is None:
   142             if timestamp is None:
   136             else:
   144             else:
   137                 self.timestamp = timestamp
   145                 self.timestamp = timestamp
   138             gzip.GzipFile.__init__(self, *args, **kw)
   146             gzip.GzipFile.__init__(self, *args, **kw)
   139 
   147 
   140         def _write_gzip_header(self):
   148         def _write_gzip_header(self):
   141             self.fileobj.write('\037\213')             # magic header
   149             self.fileobj.write('\037\213')  # magic header
   142             self.fileobj.write('\010')                 # compression method
   150             self.fileobj.write('\010')  # compression method
   143             fname = self.name
   151             fname = self.name
   144             if fname and fname.endswith('.gz'):
   152             if fname and fname.endswith('.gz'):
   145                 fname = fname[:-3]
   153                 fname = fname[:-3]
   146             flags = 0
   154             flags = 0
   147             if fname:
   155             if fname:
   160         def taropen(mode, name='', fileobj=None):
   168         def taropen(mode, name='', fileobj=None):
   161             if kind == 'gz':
   169             if kind == 'gz':
   162                 mode = mode[0:1]
   170                 mode = mode[0:1]
   163                 if not fileobj:
   171                 if not fileobj:
   164                     fileobj = open(name, mode + 'b')
   172                     fileobj = open(name, mode + 'b')
   165                 gzfileobj = self.GzipFileWithTime(name,
   173                 gzfileobj = self.GzipFileWithTime(
   166                                                   pycompat.sysstr(mode + 'b'),
   174                     name,
   167                                                   zlib.Z_BEST_COMPRESSION,
   175                     pycompat.sysstr(mode + 'b'),
   168                                                   fileobj, timestamp=mtime)
   176                     zlib.Z_BEST_COMPRESSION,
       
   177                     fileobj,
       
   178                     timestamp=mtime,
       
   179                 )
   169                 self.fileobj = gzfileobj
   180                 self.fileobj = gzfileobj
   170                 return tarfile.TarFile.taropen(
   181                 return tarfile.TarFile.taropen(
   171                     name, pycompat.sysstr(mode), gzfileobj)
   182                     name, pycompat.sysstr(mode), gzfileobj
       
   183                 )
   172             else:
   184             else:
   173                 return tarfile.open(
   185                 return tarfile.open(name, pycompat.sysstr(mode + kind), fileobj)
   174                     name, pycompat.sysstr(mode + kind), fileobj)
       
   175 
   186 
   176         if isinstance(dest, bytes):
   187         if isinstance(dest, bytes):
   177             self.z = taropen('w:', name=dest)
   188             self.z = taropen('w:', name=dest)
   178         else:
   189         else:
   179             self.z = taropen('w|', fileobj=dest)
   190             self.z = taropen('w|', fileobj=dest)
   197     def done(self):
   208     def done(self):
   198         self.z.close()
   209         self.z.close()
   199         if self.fileobj:
   210         if self.fileobj:
   200             self.fileobj.close()
   211             self.fileobj.close()
   201 
   212 
       
   213 
   202 class zipit(object):
   214 class zipit(object):
   203     '''write archive to zip file or stream.  can write uncompressed,
   215     '''write archive to zip file or stream.  can write uncompressed,
   204     or compressed with deflate.'''
   216     or compressed with deflate.'''
   205 
   217 
   206     def __init__(self, dest, mtime, compress=True):
   218     def __init__(self, dest, mtime, compress=True):
   207         if isinstance(dest, bytes):
   219         if isinstance(dest, bytes):
   208             dest = pycompat.fsdecode(dest)
   220             dest = pycompat.fsdecode(dest)
   209         self.z = zipfile.ZipFile(dest, r'w',
   221         self.z = zipfile.ZipFile(
   210                                  compress and zipfile.ZIP_DEFLATED or
   222             dest, r'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED
   211                                  zipfile.ZIP_STORED)
   223         )
   212 
   224 
   213         # Python's zipfile module emits deprecation warnings if we try
   225         # Python's zipfile module emits deprecation warnings if we try
   214         # to store files with a date before 1980.
   226         # to store files with a date before 1980.
   215         epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
   227         epoch = 315532800  # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
   216         if mtime < epoch:
   228         if mtime < epoch:
   217             mtime = epoch
   229             mtime = epoch
   218 
   230 
   219         self.mtime = mtime
   231         self.mtime = mtime
   220         self.date_time = time.gmtime(mtime)[:6]
   232         self.date_time = time.gmtime(mtime)[:6]
   231             ftype = _UNX_IFLNK
   243             ftype = _UNX_IFLNK
   232         i.external_attr = (mode | ftype) << 16
   244         i.external_attr = (mode | ftype) << 16
   233         # add "extended-timestamp" extra block, because zip archives
   245         # add "extended-timestamp" extra block, because zip archives
   234         # without this will be extracted with unexpected timestamp,
   246         # without this will be extracted with unexpected timestamp,
   235         # if TZ is not configured as GMT
   247         # if TZ is not configured as GMT
   236         i.extra += struct.pack('<hhBl',
   248         i.extra += struct.pack(
   237                                0x5455,     # block type: "extended-timestamp"
   249             '<hhBl',
   238                                1 + 4,      # size of this block
   250             0x5455,  # block type: "extended-timestamp"
   239                                1,          # "modification time is present"
   251             1 + 4,  # size of this block
   240                                int(self.mtime)) # last modification (UTC)
   252             1,  # "modification time is present"
       
   253             int(self.mtime),
       
   254         )  # last modification (UTC)
   241         self.z.writestr(i, data)
   255         self.z.writestr(i, data)
   242 
   256 
   243     def done(self):
   257     def done(self):
   244         self.z.close()
   258         self.z.close()
       
   259 
   245 
   260 
   246 class fileit(object):
   261 class fileit(object):
   247     '''write archive as files in directory.'''
   262     '''write archive as files in directory.'''
   248 
   263 
   249     def __init__(self, name, mtime):
   264     def __init__(self, name, mtime):
   264             os.utime(destfile, (self.mtime, self.mtime))
   279             os.utime(destfile, (self.mtime, self.mtime))
   265 
   280 
   266     def done(self):
   281     def done(self):
   267         pass
   282         pass
   268 
   283 
       
   284 
   269 archivers = {
   285 archivers = {
   270     'files': fileit,
   286     'files': fileit,
   271     'tar': tarit,
   287     'tar': tarit,
   272     'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'),
   288     'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'),
   273     'tgz': lambda name, mtime: tarit(name, mtime, 'gz'),
   289     'tgz': lambda name, mtime: tarit(name, mtime, 'gz'),
   274     'txz': lambda name, mtime: tarit(name, mtime, 'xz'),
   290     'txz': lambda name, mtime: tarit(name, mtime, 'xz'),
   275     'uzip': lambda name, mtime: zipit(name, mtime, False),
   291     'uzip': lambda name, mtime: zipit(name, mtime, False),
   276     'zip': zipit,
   292     'zip': zipit,
   277     }
   293 }
   278 
   294 
   279 def archive(repo, dest, node, kind, decode=True, match=None,
   295 
   280             prefix='', mtime=None, subrepos=False):
   296 def archive(
       
   297     repo,
       
   298     dest,
       
   299     node,
       
   300     kind,
       
   301     decode=True,
       
   302     match=None,
       
   303     prefix='',
       
   304     mtime=None,
       
   305     subrepos=False,
       
   306 ):
   281     '''create archive of repo as it was at node.
   307     '''create archive of repo as it was at node.
   282 
   308 
   283     dest can be name of directory, name of archive file, or file
   309     dest can be name of directory, name of archive file, or file
   284     object to write archive to.
   310     object to write archive to.
   285 
   311 
   328 
   354 
   329     files = [f for f in ctx.manifest().matches(match)]
   355     files = [f for f in ctx.manifest().matches(match)]
   330     total = len(files)
   356     total = len(files)
   331     if total:
   357     if total:
   332         files.sort()
   358         files.sort()
   333         scmutil.prefetchfiles(repo, [ctx.rev()],
   359         scmutil.prefetchfiles(
   334                               scmutil.matchfiles(repo, files))
   360             repo, [ctx.rev()], scmutil.matchfiles(repo, files)
   335         progress = repo.ui.makeprogress(_('archiving'), unit=_('files'),
   361         )
   336                                         total=total)
   362         progress = repo.ui.makeprogress(
       
   363             _('archiving'), unit=_('files'), total=total
       
   364         )
   337         progress.update(0)
   365         progress.update(0)
   338         for f in files:
   366         for f in files:
   339             ff = ctx.flags(f)
   367             ff = ctx.flags(f)
   340             write(f, 'x' in ff and 0o755 or 0o644, 'l' in ff, ctx[f].data)
   368             write(f, 'x' in ff and 0o755 or 0o644, 'l' in ff, ctx[f].data)
   341             progress.increment(item=f)
   369             progress.increment(item=f)