changeset 2116:366e6328d10e

Merge with upstream
author Thomas Arendsen Hein <thomas@intevation.de>
date Sat, 22 Apr 2006 09:19:27 +0200
parents fd77b7ee4aac (diff) 2f3e644decd7 (current diff)
children e296dee1cd9a f62195054c5b
files
diffstat 9 files changed, 320 insertions(+), 52 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/archival.py	Sat Apr 22 09:19:27 2006 +0200
@@ -0,0 +1,170 @@
+# archival.py - revision archival for mercurial
+#
+# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
+#
+# This software may be used and distributed according to the terms of
+# the GNU General Public License, incorporated herein by reference.
+
+from demandload import *
+from i18n import gettext as _
+from node import *
+demandload(globals(), 'cStringIO os stat tarfile time util zipfile')
+
+def tidyprefix(dest, prefix, suffixes):
+    '''choose prefix to use for names in archive.  make sure prefix is
+    safe for consumers.'''
+
+    if prefix:
+        prefix = prefix.replace('\\', '/')
+    else:
+        if not isinstance(dest, str):
+            raise ValueError('dest must be string if no prefix')
+        prefix = os.path.basename(dest)
+        lower = prefix.lower()
+        for sfx in suffixes:
+            if lower.endswith(sfx):
+                prefix = prefix[:-len(sfx)]
+                break
+    lpfx = os.path.normpath(util.localpath(prefix))
+    prefix = util.pconvert(lpfx)
+    if not prefix.endswith('/'):
+        prefix += '/'
+    if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix:
+        raise util.Abort(_('archive prefix contains illegal components'))
+    return prefix
+
+class tarit:
+    '''write archive to tar file or stream.  can write uncompressed,
+    or compress with gzip or bzip2.'''
+
+    def __init__(self, dest, prefix, kind=''):
+        self.prefix = tidyprefix(dest, prefix, ['.tar', '.tar.bz2', '.tar.gz',
+                                                '.tgz', 'tbz2'])
+        self.mtime = int(time.time())
+        if isinstance(dest, str):
+            self.z = tarfile.open(dest, mode='w:'+kind)
+        else:
+            self.z = tarfile.open(mode='w|'+kind, fileobj=dest)
+
+    def addfile(self, name, mode, data):
+        i = tarfile.TarInfo(self.prefix + name)
+        i.mtime = self.mtime
+        i.size = len(data)
+        i.mode = mode
+        self.z.addfile(i, cStringIO.StringIO(data))
+
+    def done(self):
+        self.z.close()
+
+class tellable:
+    '''provide tell method for zipfile.ZipFile when writing to http
+    response file object.'''
+
+    def __init__(self, fp):
+        self.fp = fp
+        self.offset = 0
+
+    def __getattr__(self, key):
+        return getattr(self.fp, key)
+
+    def write(self, s):
+        self.fp.write(s)
+        self.offset += len(s)
+
+    def tell(self):
+        return self.offset
+
+class zipit:
+    '''write archive to zip file or stream.  can write uncompressed,
+    or compressed with deflate.'''
+
+    def __init__(self, dest, prefix, compress=True):
+        self.prefix = tidyprefix(dest, prefix, ('.zip',))
+        if not isinstance(dest, str) and not hasattr(dest, 'tell'):
+            dest = tellable(dest)
+        self.z = zipfile.ZipFile(dest, 'w',
+                                 compress and zipfile.ZIP_DEFLATED or
+                                 zipfile.ZIP_STORED)
+        self.date_time = time.gmtime(time.time())[:6]
+
+    def addfile(self, name, mode, data):
+        i = zipfile.ZipInfo(self.prefix + name, self.date_time)
+        i.compress_type = self.z.compression
+        i.flag_bits = 0x08
+        # unzip will not honor unix file modes unless file creator is
+        # set to unix (id 3).
+        i.create_system = 3
+        i.external_attr = (mode | stat.S_IFREG) << 16L
+        self.z.writestr(i, data)
+
+    def done(self):
+        self.z.close()
+
+class fileit:
+    '''write archive as files in directory.'''
+
+    def __init__(self, name, prefix):
+        if prefix:
+            raise util.Abort(_('cannot give prefix when archiving to files'))
+        self.basedir = name
+        self.dirs = {}
+        self.oflags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY |
+                       getattr(os, 'O_BINARY', 0) |
+                       getattr(os, 'O_NOFOLLOW', 0))
+
+    def addfile(self, name, mode, data):
+        destfile = os.path.join(self.basedir, name)
+        destdir = os.path.dirname(destfile)
+        if destdir not in self.dirs:
+            if not os.path.isdir(destdir):
+                os.makedirs(destdir)
+            self.dirs[destdir] = 1
+        os.fdopen(os.open(destfile, self.oflags, mode), 'wb').write(data)
+
+    def done(self):
+        pass
+
+archivers = {
+    'files': fileit,
+    'tar': tarit,
+    'tbz2': lambda name, prefix: tarit(name, prefix, 'bz2'),
+    'tgz': lambda name, prefix: tarit(name, prefix, 'gz'),
+    'uzip': lambda name, prefix: zipit(name, prefix, False),
+    'zip': zipit,
+    }
+
+def archive(repo, dest, node, kind, decode=True, matchfn=None,
+            prefix=None):
+    '''create archive of repo as it was at node.
+
+    dest can be name of directory, name of archive file, or file
+    object to write archive to.
+
+    kind is type of archive to create.
+
+    decode tells whether to put files through decode filters from
+    hgrc.
+
+    matchfn is function to filter names of files to write to archive.
+
+    prefix is name of path to put before every archive member.'''
+
+    def write(name, mode, data):
+        if matchfn and not matchfn(name): return
+        if decode:
+            fp = cStringIO.StringIO()
+            repo.wwrite(None, data, fp)
+            data = fp.getvalue()
+        archiver.addfile(name, mode, data)
+
+    archiver = archivers[kind](dest, prefix)
+    mn = repo.changelog.read(node)[0]
+    mf = repo.manifest.read(mn).items()
+    mff = repo.manifest.readflags(mn)
+    mf.sort()
+    write('.hg_archival.txt', 0644,
+          'repo: %s\nnode: %s\n' % (hex(repo.changelog.node(0)), hex(node)))
+    for filename, filenode in mf:
+        write(filename, mff[filename] and 0755 or 0644,
+              repo.file(filename).read(filenode))
+    archiver.done()
--- a/mercurial/commands.py	Fri Apr 21 16:30:49 2006 -0500
+++ b/mercurial/commands.py	Sat Apr 22 09:19:27 2006 +0200
@@ -12,7 +12,7 @@
 demandload(globals(), "fancyopts ui hg util lock revlog templater bundlerepo")
 demandload(globals(), "fnmatch hgweb mdiff random signal tempfile time")
 demandload(globals(), "traceback errno socket version struct atexit sets bz2")
-demandload(globals(), "changegroup")
+demandload(globals(), "archival changegroup")
 
 class UnknownCommand(Exception):
     """Exception raised if command is not in the command table."""
@@ -890,6 +890,46 @@
             for p, l in zip(zip(*pieces), lines):
                 ui.write("%s: %s" % (" ".join(p), l[1]))
 
+def archive(ui, repo, dest, **opts):
+    '''create unversioned archive of a repository revision
+
+    By default, the revision used is the parent of the working
+    directory; use "-r" to specify a different revision.
+
+    To specify the type of archive to create, use "-t".  Valid
+    types are:
+
+    "files" (default): a directory full of files
+    "tar": tar archive, uncompressed
+    "tbz2": tar archive, compressed using bzip2
+    "tgz": tar archive, compressed using gzip
+    "uzip": zip archive, uncompressed
+    "zip": zip archive, compressed using deflate
+
+    The exact name of the destination archive or directory is given
+    using a format string; see "hg help export" for details.
+
+    Each member added to an archive file has a directory prefix
+    prepended.  Use "-p" to specify a format string for the prefix.
+    The default is the basename of the archive, with suffixes removed.
+    '''
+
+    if opts['rev']:
+        node = repo.lookup(opts['rev'])
+    else:
+        node, p2 = repo.dirstate.parents()
+        if p2 != nullid:
+            raise util.Abort(_('uncommitted merge - please provide a '
+                               'specific revision'))
+
+    dest = make_filename(repo, repo.changelog, dest, node)
+    prefix = make_filename(repo, repo.changelog, opts['prefix'], node)
+    if os.path.realpath(dest) == repo.root:
+        raise util.Abort(_('repository root cannot be destination'))
+    _, matchfn, _ = matchpats(repo, [], opts)
+    archival.archive(repo, dest, node, opts.get('type') or 'files',
+                    not opts['no_decode'], matchfn, prefix)
+
 def bundle(ui, repo, fname, dest="default-push", **opts):
     """create a changegroup file
 
@@ -2839,6 +2879,15 @@
           ('I', 'include', [], _('include names matching the given patterns')),
           ('X', 'exclude', [], _('exclude names matching the given patterns'))],
          _('hg annotate [-r REV] [-a] [-u] [-d] [-n] [-c] FILE...')),
+    'archive':
+        (archive,
+         [('', 'no-decode', None, _('do not pass files through decoders')),
+          ('p', 'prefix', '', _('directory prefix for files in archive')),
+          ('r', 'rev', '', _('revision to distribute')),
+          ('t', 'type', '', _('type of distribution to create')),
+          ('I', 'include', [], _('include names matching the given patterns')),
+          ('X', 'exclude', [], _('exclude names matching the given patterns'))],
+         _('hg archive [OPTION]... DEST')),
     "bundle":
         (bundle,
          [('f', 'force', None,
@@ -3249,7 +3298,7 @@
     return (cmd, cmd and i[0] or None, args, options, cmdoptions)
 
 def dispatch(args):
-    for name in 'SIGTERM', 'SIGHUP', 'SIGBREAK':
+    for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
         num = getattr(signal, name, None)
         if num: signal.signal(num, catchterm)
 
--- a/mercurial/hgweb.py	Fri Apr 21 16:30:49 2006 -0500
+++ b/mercurial/hgweb.py	Sat Apr 22 09:19:27 2006 +0200
@@ -10,8 +10,8 @@
 import mimetypes
 from demandload import demandload
 demandload(globals(), "mdiff time re socket zlib errno ui hg ConfigParser")
-demandload(globals(), "zipfile tempfile StringIO tarfile BaseHTTPServer util")
-demandload(globals(), "mimetypes templater")
+demandload(globals(), "tempfile StringIO BaseHTTPServer util")
+demandload(globals(), "archival mimetypes templater")
 from node import *
 from i18n import gettext as _
 
@@ -682,55 +682,23 @@
                      child=self.siblings(cl.children(n), cl.rev),
                      diff=diff)
 
-    def archive(self, req, cnode, type):
-        cs = self.repo.changelog.read(cnode)
-        mnode = cs[0]
-        mf = self.repo.manifest.read(mnode)
-        rev = self.repo.manifest.rev(mnode)
-        reponame = re.sub(r"\W+", "-", self.reponame)
-        name = "%s-%s/" % (reponame, short(cnode))
-
-        files = mf.keys()
-        files.sort()
-
-        if type == 'zip':
-            tmp = tempfile.mkstemp()[1]
-            try:
-                zf = zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED)
-
-                for f in files:
-                    zf.writestr(name + f, self.repo.file(f).read(mf[f]))
-                zf.close()
+    archive_specs = {
+        'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'),
+        'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'),
+        'zip': ('application/zip', 'zip', '.zip', None),
+        }
 
-                f = open(tmp, 'r')
-                req.httphdr('application/zip', name[:-1] + '.zip',
-                        os.path.getsize(tmp))
-                req.write(f.read())
-                f.close()
-            finally:
-                os.unlink(tmp)
-
-        else:
-            tf = tarfile.TarFile.open(mode='w|' + type, fileobj=req.out)
-            mff = self.repo.manifest.readflags(mnode)
-            mtime = int(time.time())
-
-            if type == "gz":
-                encoding = "gzip"
-            else:
-                encoding = "x-bzip2"
-            req.header([('Content-type', 'application/x-tar'),
-                    ('Content-disposition', 'attachment; filename=%s%s%s' %
-                        (name[:-1], '.tar.', type)),
-                    ('Content-encoding', encoding)])
-            for fname in files:
-                rcont = self.repo.file(fname).read(mf[fname])
-                finfo = tarfile.TarInfo(name + fname)
-                finfo.mtime = mtime
-                finfo.size = len(rcont)
-                finfo.mode = mff[fname] and 0755 or 0644
-                tf.addfile(finfo, StringIO.StringIO(rcont))
-            tf.close()
+    def archive(self, req, cnode, type):
+        reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
+        name = "%s-%s" % (reponame, short(cnode))
+        mimetype, artype, extension, encoding = self.archive_specs[type]
+        headers = [('Content-type', mimetype),
+                   ('Content-disposition', 'attachment; filename=%s%s' %
+                    (name, extension))]
+        if encoding:
+            headers.append(('Content-encoding', encoding))
+        req.header(headers)
+        archival.archive(self.repo, req.out, cnode, artype, prefix=name)
 
     # add tags to things
     # tags -> list of changesets corresponding to tags
--- a/mercurial/util.py	Fri Apr 21 16:30:49 2006 -0500
+++ b/mercurial/util.py	Sat Apr 22 09:19:27 2006 +0200
@@ -215,6 +215,30 @@
     elif name == root:
         return ''
     else:
+        # Determine whether `name' is in the hierarchy at or beneath `root',
+        # by iterating name=dirname(name) until that causes no change (can't
+        # check name == '/', because that doesn't work on windows).  For each
+        # `name', compare dev/inode numbers.  If they match, the list `rel'
+        # holds the reversed list of components making up the relative file
+        # name we want.
+        root_st = os.stat(root)
+        rel = []
+        while True:
+            try:
+                name_st = os.stat(name)
+            except OSError:
+                break
+            if os.path.samestat(name_st, root_st):
+                rel.reverse()
+                name = os.path.join(*rel)
+                audit_path(name)
+                return pconvert(name)
+            dirname, basename = os.path.split(name)
+            rel.append(basename)
+            if dirname == name:
+                break
+            name = dirname
+
         raise Abort('%s not under root' % myname)
 
 def matcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
--- a/tests/test-archive	Fri Apr 21 16:30:49 2006 -0500
+++ b/tests/test-archive	Sat Apr 22 09:19:27 2006 +0200
@@ -36,3 +36,18 @@
 
 kill `cat hg.pid`
 sleep 1 # wait for server to scream and die
+
+hg archive -t tar test.tar
+tar tf test.tar
+
+hg archive -t tbz2 -X baz test.tar.bz2
+bunzip2 -dc test.tar.bz2 | tar tf -
+
+hg archive -t tgz -p %b-%h test-%h.tar.gz
+gzip -dc test-$QTIP.tar.gz | tar tf - | sed "s/$QTIP/TIP/"
+
+hg archive -t zip -p /illegal test.zip
+hg archive -t zip -p very/../bad test.zip
+
+hg archive -t zip -r 2 test.zip
+unzip -t test.zip
--- a/tests/test-archive.out	Fri Apr 21 16:30:49 2006 -0500
+++ b/tests/test-archive.out	Sat Apr 22 09:19:27 2006 +0200
@@ -1,14 +1,35 @@
 adding foo
 adding bar
 adding baz/bletch
+test-archive-TIP/.hg_archival.txt
 test-archive-TIP/bar
 test-archive-TIP/baz/bletch
 test-archive-TIP/foo
+test-archive-TIP/.hg_archival.txt
 test-archive-TIP/bar
 test-archive-TIP/baz/bletch
 test-archive-TIP/foo
 Archive:  archive.zip
+    testing: test-archive-TIP/.hg_archival.txt   OK
     testing: test-archive-TIP/bar   OK
     testing: test-archive-TIP/baz/bletch   OK
     testing: test-archive-TIP/foo   OK
 No errors detected in compressed data of archive.zip.
+test/.hg_archival.txt
+test/bar
+test/baz/bletch
+test/foo
+test/.hg_archival.txt
+test/bar
+test/foo
+test-TIP/.hg_archival.txt
+test-TIP/bar
+test-TIP/baz/bletch
+test-TIP/foo
+abort: archive prefix contains illegal components
+Archive:  test.zip
+    testing: test/.hg_archival.txt    OK
+    testing: test/bar                 OK
+    testing: test/baz/bletch          OK
+    testing: test/foo                 OK
+No errors detected in compressed data of test.zip.
--- a/tests/test-help.out	Fri Apr 21 16:30:49 2006 -0500
+++ b/tests/test-help.out	Sat Apr 22 09:19:27 2006 +0200
@@ -41,6 +41,7 @@
  add         add the specified files on the next commit
  addremove   add all new files, delete all missing files
  annotate    show changeset information per file line
+ archive     create unversioned archive of a repository revision
  bundle      create a changegroup file
  cat         output the latest or given revisions of files
  clone       make a copy of an existing repository
@@ -83,6 +84,7 @@
  add         add the specified files on the next commit
  addremove   add all new files, delete all missing files
  annotate    show changeset information per file line
+ archive     create unversioned archive of a repository revision
  bundle      create a changegroup file
  cat         output the latest or given revisions of files
  clone       make a copy of an existing repository
--- a/tests/test-symlinks	Fri Apr 21 16:30:49 2006 -0500
+++ b/tests/test-symlinks	Sat Apr 22 09:19:27 2006 +0200
@@ -40,3 +40,18 @@
 # it should show a.c, dir/a.o and dir/b.o deleted
 hg status
 hg status a.c
+
+echo '# test absolute path through symlink outside repo'
+cd ..
+p=`pwd`
+hg init x
+ln -s x y
+cd x
+touch f
+hg add f
+hg status $p/y/f
+
+echo '# try symlink outside repo to file inside'
+ln -s x/f ../z
+# this should fail
+hg status ../z && { echo hg mistakenly exited with status 0; exit 1; } || :
--- a/tests/test-symlinks.out	Fri Apr 21 16:30:49 2006 -0500
+++ b/tests/test-symlinks.out	Sat Apr 22 09:19:27 2006 +0200
@@ -9,3 +9,7 @@
 ? .hgignore
 a.c: unsupported file type (type is fifo)
 ! a.c
+# test absolute path through symlink outside repo
+A f
+# try symlink outside repo to file inside
+abort: ../z not under root