archival.py
393 lines
| 11.2 KiB
| text/x-python
|
PythonLexer
/ mercurial / archival.py
Vadim Gelfer
|
r2112 | # archival.py - revision archival for mercurial | ||
# | ||||
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | ||||
# | ||||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Vadim Gelfer
|
r2112 | |||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Gregory Szorc
|
r25916 | |||
import gzip | ||||
import os | ||||
FUJIWARA Katsunori
|
r17628 | import struct | ||
Gregory Szorc
|
r25916 | import tarfile | ||
import time | ||||
Matt Harbison
|
r52752 | import typing | ||
Gregory Szorc
|
r25916 | import zipfile | ||
import zlib | ||||
Matt Harbison
|
r52752 | from typing import ( | ||
Optional, | ||||
) | ||||
Gregory Szorc
|
r25916 | from .i18n import _ | ||
Augie Fackler
|
r43346 | from .node import nullrev | ||
Gregory Szorc
|
r43355 | from .pycompat import open | ||
Gregory Szorc
|
r25916 | |||
from . import ( | ||||
error, | ||||
Matt Harbison
|
r33544 | formatter, | ||
Gregory Szorc
|
r25916 | match as matchmod, | ||
Augie Fackler
|
r36724 | pycompat, | ||
Matt Harbison
|
r36156 | scmutil, | ||
Gregory Szorc
|
r25916 | util, | ||
Pierre-Yves David
|
r31235 | vfs as vfsmod, | ||
Gregory Szorc
|
r25916 | ) | ||
Augie Fackler
|
r43346 | |||
Matt Harbison
|
r48825 | from .utils import stringutil | ||
Matt Harbison
|
r52752 | if typing.TYPE_CHECKING: | ||
from . import ( | ||||
localrepo, | ||||
) | ||||
timeless
|
r28861 | stringio = util.stringio | ||
Vadim Gelfer
|
r2112 | |||
Mads Kiilerich
|
r17429 | # from unzip source code: | ||
_UNX_IFREG = 0x8000 | ||||
Augie Fackler
|
r43346 | _UNX_IFLNK = 0xA000 | ||
Mads Kiilerich
|
r17429 | |||
Martin Geisler
|
r11558 | def tidyprefix(dest, kind, prefix): | ||
Augie Fackler
|
r46554 | """choose prefix to use for names in archive. make sure prefix is | ||
safe for consumers.""" | ||||
Vadim Gelfer
|
r2112 | |||
if prefix: | ||||
Shun-ichi GOTO
|
r5842 | prefix = util.normpath(prefix) | ||
Vadim Gelfer
|
r2112 | else: | ||
Pulkit Goyal
|
r36455 | if not isinstance(dest, bytes): | ||
Augie Fackler
|
r43347 | raise ValueError(b'dest must be string if no prefix') | ||
Vadim Gelfer
|
r2112 | prefix = os.path.basename(dest) | ||
lower = prefix.lower() | ||||
Martin Geisler
|
r11558 | for sfx in exts.get(kind, []): | ||
Vadim Gelfer
|
r2112 | if lower.endswith(sfx): | ||
Augie Fackler
|
r43346 | prefix = prefix[: -len(sfx)] | ||
Vadim Gelfer
|
r2112 | break | ||
lpfx = os.path.normpath(util.localpath(prefix)) | ||||
prefix = util.pconvert(lpfx) | ||||
Augie Fackler
|
r43347 | if not prefix.endswith(b'/'): | ||
prefix += b'/' | ||||
Matt Harbison
|
r24953 | # Drop the leading '.' path component if present, so Windows can read the | ||
# zip files (issue4634) | ||||
Augie Fackler
|
r43347 | if prefix.startswith(b'./'): | ||
Matt Harbison
|
r24953 | prefix = prefix[2:] | ||
Augie Fackler
|
r43347 | if prefix.startswith(b'../') or os.path.isabs(lpfx) or b'/../' in prefix: | ||
raise error.Abort(_(b'archive prefix contains illegal components')) | ||||
Vadim Gelfer
|
r2112 | return prefix | ||
Augie Fackler
|
r43346 | |||
Martin Geisler
|
r11557 | exts = { | ||
Augie Fackler
|
r43347 | b'tar': [b'.tar'], | ||
b'tbz2': [b'.tbz2', b'.tar.bz2'], | ||||
b'tgz': [b'.tgz', b'.tar.gz'], | ||||
b'zip': [b'.zip'], | ||||
b'txz': [b'.txz', b'.tar.xz'], | ||||
Augie Fackler
|
r43346 | } | ||
Martin Geisler
|
r11557 | |||
def guesskind(dest): | ||||
Gregory Szorc
|
r49768 | for kind, extensions in exts.items(): | ||
Augie Fackler
|
r25149 | if any(dest.endswith(ext) for ext in extensions): | ||
Martin Geisler
|
r11557 | return kind | ||
return None | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r24681 | def _rootctx(repo): | ||
# repo[0] may be hidden | ||||
for rev in repo: | ||||
return repo[rev] | ||||
Martin von Zweigbergk
|
r39930 | return repo[nullrev] | ||
Yuya Nishihara
|
r24681 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35923 | # {tags} on ctx includes local tags and 'tip', with no current way to limit | ||
# that to global tags. Therefore, use {latesttag} as a substitute when | ||||
# the distance is 0, since that will be the list of global tags on ctx. | ||||
_defaultmetatemplate = br''' | ||||
repo: {root} | ||||
node: {ifcontains(rev, revset("wdir()"), "{p1node}{dirty}", "{node}")} | ||||
branch: {branch|utf8} | ||||
{ifeq(latesttagdistance, 0, join(latesttag % "tag: {tag}", "\n"), | ||||
separate("\n", | ||||
join(latesttag % "latesttag: {tag}", "\n"), | ||||
"latesttagdistance: {latesttagdistance}", | ||||
"changessincelatesttag: {changessincelatesttag}"))} | ||||
Augie Fackler
|
r43346 | '''[ | ||
1: | ||||
] # drop leading '\n' | ||||
Yuya Nishihara
|
r35923 | |||
Yuya Nishihara
|
r24678 | def buildmetadata(ctx): | ||
'''build content of .hg_archival.txt''' | ||||
repo = ctx.repo() | ||||
Matt Harbison
|
r33544 | |||
opts = { | ||||
Augie Fackler
|
r43347 | b'template': repo.ui.config( | ||
b'experimental', b'archivemetatemplate', _defaultmetatemplate | ||||
Augie Fackler
|
r43346 | ) | ||
Matt Harbison
|
r33544 | } | ||
out = util.stringio() | ||||
Yuya Nishihara
|
r24678 | |||
Augie Fackler
|
r43347 | fm = formatter.formatter(repo.ui, out, b'archive', opts) | ||
Matt Harbison
|
r33544 | fm.startitem() | ||
fm.context(ctx=ctx) | ||||
fm.data(root=_rootctx(repo).hex()) | ||||
if ctx.rev() is None: | ||||
Augie Fackler
|
r43347 | dirty = b'' | ||
Matt Harbison
|
r33544 | if ctx.dirty(missing=True): | ||
Augie Fackler
|
r43347 | dirty = b'+' | ||
Matt Harbison
|
r33544 | fm.data(dirty=dirty) | ||
fm.end() | ||||
return out.getvalue() | ||||
Martin Geisler
|
r11557 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r49801 | class tarit: | ||
Augie Fackler
|
r46554 | """write archive to tar file or stream. can write uncompressed, | ||
or compress with gzip or bzip2.""" | ||||
Vadim Gelfer
|
r2112 | |||
Augie Fackler
|
r43347 | def __init__(self, dest, mtime, kind=b''): | ||
Vadim Gelfer
|
r2477 | self.mtime = mtime | ||
Dan Villiom Podlaski Christiansen
|
r13400 | self.fileobj = None | ||
csaba.henk@creo.hu
|
r4652 | |||
Augie Fackler
|
r43347 | def taropen(mode, name=b'', fileobj=None): | ||
if kind == b'gz': | ||||
Pulkit Goyal
|
r36465 | mode = mode[0:1] | ||
csaba.henk@creo.hu
|
r4652 | if not fileobj: | ||
Augie Fackler
|
r43347 | fileobj = open(name, mode + b'b') | ||
Gregory Szorc
|
r49737 | gzfileobj = gzip.GzipFile( | ||
Augie Fackler
|
r43346 | name, | ||
Augie Fackler
|
r43347 | pycompat.sysstr(mode + b'b'), | ||
Augie Fackler
|
r43346 | zlib.Z_BEST_COMPRESSION, | ||
fileobj, | ||||
r44980 | mtime=mtime, | |||
Augie Fackler
|
r43346 | ) | ||
Dan Villiom Podlaski Christiansen
|
r13400 | self.fileobj = gzfileobj | ||
Matt Harbison
|
r52754 | return tarfile.TarFile.taropen(name, "w", gzfileobj) | ||
csaba.henk@creo.hu
|
r4652 | else: | ||
Manuel Jacob
|
r45601 | try: | ||
return tarfile.open( | ||||
name, pycompat.sysstr(mode + kind), fileobj | ||||
) | ||||
except tarfile.CompressionError as e: | ||||
Matt Harbison
|
r48825 | raise error.Abort(stringutil.forcebytestr(e)) | ||
csaba.henk@creo.hu
|
r4652 | |||
Augie Fackler
|
r36726 | if isinstance(dest, bytes): | ||
Augie Fackler
|
r43347 | self.z = taropen(b'w:', name=dest) | ||
Vadim Gelfer
|
r2112 | else: | ||
Augie Fackler
|
r43347 | self.z = taropen(b'w|', fileobj=dest) | ||
Vadim Gelfer
|
r2112 | |||
Alexis S. L. Carvalho
|
r4831 | def addfile(self, name, mode, islink, data): | ||
Augie Fackler
|
r36724 | name = pycompat.fsdecode(name) | ||
Martin Geisler
|
r11558 | i = tarfile.TarInfo(name) | ||
Vadim Gelfer
|
r2112 | i.mtime = self.mtime | ||
i.size = len(data) | ||||
Alexis S. L. Carvalho
|
r4831 | if islink: | ||
i.type = tarfile.SYMTYPE | ||||
Gregory Szorc
|
r25658 | i.mode = 0o777 | ||
Augie Fackler
|
r36724 | i.linkname = pycompat.fsdecode(data) | ||
Alexis S. L. Carvalho
|
r4831 | data = None | ||
Peter van Dijk
|
r7770 | i.size = 0 | ||
Alexis S. L. Carvalho
|
r4831 | else: | ||
i.mode = mode | ||||
timeless
|
r28861 | data = stringio(data) | ||
Alexis S. L. Carvalho
|
r4831 | self.z.addfile(i, data) | ||
Vadim Gelfer
|
r2112 | |||
def done(self): | ||||
self.z.close() | ||||
Dan Villiom Podlaski Christiansen
|
r13400 | if self.fileobj: | ||
self.fileobj.close() | ||||
Vadim Gelfer
|
r2112 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r49801 | class zipit: | ||
Augie Fackler
|
r46554 | """write archive to zip file or stream. can write uncompressed, | ||
or compressed with deflate.""" | ||||
Vadim Gelfer
|
r2112 | |||
Martin Geisler
|
r11558 | def __init__(self, dest, mtime, compress=True): | ||
Augie Fackler
|
r40283 | if isinstance(dest, bytes): | ||
dest = pycompat.fsdecode(dest) | ||||
Augie Fackler
|
r43346 | self.z = zipfile.ZipFile( | ||
Augie Fackler
|
r43906 | dest, 'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED | ||
Augie Fackler
|
r43346 | ) | ||
Martin Geisler
|
r12319 | |||
# Python's zipfile module emits deprecation warnings if we try | ||||
# to store files with a date before 1980. | ||||
Augie Fackler
|
r43346 | epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0)) | ||
Martin Geisler
|
r12319 | if mtime < epoch: | ||
mtime = epoch | ||||
FUJIWARA Katsunori
|
r17628 | self.mtime = mtime | ||
Vadim Gelfer
|
r2477 | self.date_time = time.gmtime(mtime)[:6] | ||
Vadim Gelfer
|
r2112 | |||
Alexis S. L. Carvalho
|
r4831 | def addfile(self, name, mode, islink, data): | ||
Augie Fackler
|
r36724 | i = zipfile.ZipInfo(pycompat.fsdecode(name), self.date_time) | ||
Augie Fackler
|
r43788 | i.compress_type = self.z.compression # pytype: disable=attribute-error | ||
Vadim Gelfer
|
r2112 | # unzip will not honor unix file modes unless file creator is | ||
# set to unix (id 3). | ||||
i.create_system = 3 | ||||
Mads Kiilerich
|
r17429 | ftype = _UNX_IFREG | ||
Alexis S. L. Carvalho
|
r4831 | if islink: | ||
Gregory Szorc
|
r25658 | mode = 0o777 | ||
Mads Kiilerich
|
r17429 | ftype = _UNX_IFLNK | ||
Pulkit Goyal
|
r29890 | i.external_attr = (mode | ftype) << 16 | ||
FUJIWARA Katsunori
|
r17628 | # add "extended-timestamp" extra block, because zip archives | ||
# without this will be extracted with unexpected timestamp, | ||||
# if TZ is not configured as GMT | ||||
Augie Fackler
|
r43346 | i.extra += struct.pack( | ||
Augie Fackler
|
r43347 | b'<hhBl', | ||
Augie Fackler
|
r43346 | 0x5455, # block type: "extended-timestamp" | ||
1 + 4, # size of this block | ||||
1, # "modification time is present" | ||||
int(self.mtime), | ||||
) # last modification (UTC) | ||||
Vadim Gelfer
|
r2112 | self.z.writestr(i, data) | ||
def done(self): | ||||
self.z.close() | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r49801 | class fileit: | ||
Vadim Gelfer
|
r2112 | '''write archive as files in directory.''' | ||
Martin Geisler
|
r11558 | def __init__(self, name, mtime): | ||
Vadim Gelfer
|
r2112 | self.basedir = name | ||
Pierre-Yves David
|
r31235 | self.opener = vfsmod.vfs(self.basedir) | ||
James May
|
r35203 | self.mtime = mtime | ||
Vadim Gelfer
|
r2112 | |||
Alexis S. L. Carvalho
|
r4831 | def addfile(self, name, mode, islink, data): | ||
if islink: | ||||
self.opener.symlink(data, name) | ||||
return | ||||
Augie Fackler
|
r43347 | f = self.opener(name, b"w", atomictemp=False) | ||
Alexis S. L. Carvalho
|
r4830 | f.write(data) | ||
Greg Ward
|
r15057 | f.close() | ||
Vadim Gelfer
|
r2112 | destfile = os.path.join(self.basedir, name) | ||
Alexis S. L. Carvalho
|
r4830 | os.chmod(destfile, mode) | ||
James May
|
r35203 | if self.mtime is not None: | ||
os.utime(destfile, (self.mtime, self.mtime)) | ||||
Vadim Gelfer
|
r2112 | |||
def done(self): | ||||
pass | ||||
Augie Fackler
|
r43346 | |||
Vadim Gelfer
|
r2112 | archivers = { | ||
Augie Fackler
|
r43347 | b'files': fileit, | ||
b'tar': tarit, | ||||
b'tbz2': lambda name, mtime: tarit(name, mtime, b'bz2'), | ||||
b'tgz': lambda name, mtime: tarit(name, mtime, b'gz'), | ||||
b'txz': lambda name, mtime: tarit(name, mtime, b'xz'), | ||||
b'uzip': lambda name, mtime: zipit(name, mtime, False), | ||||
b'zip': zipit, | ||||
Augie Fackler
|
r43346 | } | ||
Vadim Gelfer
|
r2112 | |||
Augie Fackler
|
r43346 | def archive( | ||
Matt Harbison
|
r52752 | repo: "localrepo.localrepository", | ||
dest, # TODO: should be bytes, but could be Callable | ||||
Augie Fackler
|
r43346 | node, | ||
Matt Harbison
|
r52752 | kind: bytes, | ||
decode: bool = True, | ||||
Augie Fackler
|
r43346 | match=None, | ||
Matt Harbison
|
r52752 | prefix: bytes = b'', | ||
mtime: Optional[float] = None, | ||||
subrepos: bool = False, | ||||
) -> int: | ||||
Augie Fackler
|
r46554 | """create archive of repo as it was at node. | ||
Vadim Gelfer
|
r2112 | |||
Joerg Sonnenberger
|
r52731 | dest can be name of directory, name of archive file, a callable, or file | ||
object to write archive to. If it is a callable, it will called to open | ||||
the actual file object before the first archive member is written. | ||||
Vadim Gelfer
|
r2112 | |||
kind is type of archive to create. | ||||
decode tells whether to put files through decode filters from | ||||
hgrc. | ||||
Martin von Zweigbergk
|
r40443 | match is a matcher to filter names of files to write to archive. | ||
Vadim Gelfer
|
r2112 | |||
James May
|
r35203 | prefix is name of path to put before every archive member. | ||
mtime is the modified time, in seconds, or None to use the changeset time. | ||||
subrepos tells whether to include subrepos. | ||||
Augie Fackler
|
r46554 | """ | ||
Vadim Gelfer
|
r2112 | |||
Augie Fackler
|
r43347 | if kind == b'files': | ||
Martin Geisler
|
r11558 | if prefix: | ||
Augie Fackler
|
r43347 | raise error.Abort(_(b'cannot give prefix when archiving to files')) | ||
Martin Geisler
|
r11558 | else: | ||
prefix = tidyprefix(dest, kind, prefix) | ||||
Joerg Sonnenberger
|
r52731 | archiver = None | ||
ctx = repo[node] | ||||
def opencallback(): | ||||
"""Return the archiver instance, creating it if necessary. | ||||
This function is called when the first actual entry is created. | ||||
It may be called multiple times from different layers. | ||||
When serving the archive via hgweb, no errors should happen after | ||||
this point. | ||||
""" | ||||
nonlocal archiver | ||||
if archiver is None: | ||||
if callable(dest): | ||||
output = dest() | ||||
else: | ||||
output = dest | ||||
archiver = archivers[kind](output, mtime or ctx.date()[0]) | ||||
assert archiver is not None | ||||
if repo.ui.configbool(b"ui", b"archivemeta"): | ||||
metaname = b'.hg_archival.txt' | ||||
if match(metaname): | ||||
write(metaname, 0o644, False, lambda: buildmetadata(ctx)) | ||||
return archiver | ||||
Alexis S. L. Carvalho
|
r4951 | def write(name, mode, islink, getdata): | ||
Joerg Sonnenberger
|
r52731 | if archiver is None: | ||
opencallback() | ||||
assert archiver is not None, "archive should be opened by now" | ||||
Alexis S. L. Carvalho
|
r4951 | data = getdata() | ||
Vadim Gelfer
|
r2112 | if decode: | ||
Matt Mackall
|
r4005 | data = repo.wwritedata(name, data) | ||
Martin Geisler
|
r11558 | archiver.addfile(prefix + name, mode, islink, data) | ||
Vadim Gelfer
|
r2112 | |||
Dirkjan Ochtman
|
r6019 | if kind not in archivers: | ||
Augie Fackler
|
r43347 | raise error.Abort(_(b"unknown archive type '%s'") % kind) | ||
Matt Mackall
|
r6749 | |||
Martin von Zweigbergk
|
r40444 | if not match: | ||
match = scmutil.matchall(repo) | ||||
Augie Fackler
|
r44766 | files = list(ctx.manifest().walk(match)) | ||
Thomas Arendsen Hein
|
r16919 | total = len(files) | ||
Angel Ezquerra
|
r18967 | if total: | ||
files.sort() | ||||
Augie Fackler
|
r43346 | scmutil.prefetchfiles( | ||
Rodrigo Damazio Bovendorp
|
r45632 | repo, [(ctx.rev(), scmutil.matchfiles(repo, files))] | ||
Augie Fackler
|
r43346 | ) | ||
progress = repo.ui.makeprogress( | ||||
Augie Fackler
|
r43347 | _(b'archiving'), unit=_(b'files'), total=total | ||
Augie Fackler
|
r43346 | ) | ||
Martin von Zweigbergk
|
r38400 | progress.update(0) | ||
for f in files: | ||||
Angel Ezquerra
|
r18967 | ff = ctx.flags(f) | ||
Augie Fackler
|
r43347 | write(f, b'x' in ff and 0o755 or 0o644, b'l' in ff, ctx[f].data) | ||
Martin von Zweigbergk
|
r38400 | progress.increment(item=f) | ||
progress.complete() | ||||
Martin Geisler
|
r12323 | |||
if subrepos: | ||||
Mads Kiilerich
|
r18364 | for subpath in sorted(ctx.substate): | ||
Matt Harbison
|
r25601 | sub = ctx.workingsub(subpath) | ||
Martin von Zweigbergk
|
r40443 | submatch = matchmod.subdirmatcher(subpath, match) | ||
Augie Fackler
|
r43347 | subprefix = prefix + subpath + b'/' | ||
Joerg Sonnenberger
|
r52731 | total += sub.archive(opencallback, subprefix, submatch, decode) | ||
Angel Ezquerra
|
r18967 | |||
if total == 0: | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b'no files match the archive pattern')) | ||
Martin Geisler
|
r12323 | |||
Joerg Sonnenberger
|
r52731 | assert archiver is not None, "archive should have been opened before" | ||
Vadim Gelfer
|
r2112 | archiver.done() | ||
Angel Ezquerra
|
r18967 | return total | ||