add "archive" command, like "cvs export" only better.
most code in mercurial/archival.py module, for sharing with hgweb.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/archival.py Fri Apr 21 15:27:57 2006 -0700
@@ -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 18:47:55 2006 +0200
+++ b/mercurial/commands.py Fri Apr 21 15:27:57 2006 -0700
@@ -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)