Mercurial > public > mercurial-scm > hg
diff mercurial/archival.py @ 51834:3b8d92f71d92
archive: defer opening the output until a file is matched
Before, if no file is matched, an error is thrown, but the archive is
created anyway. When using hgweb, an error 500 is returned as the
response body already exists when the error is seen.
Afterwards, the archive is created before the first match is emitted.
If no match is found, no archive is created. This is more consistent
behavior as an empty archive is not a representable in all output
formats, e.g. tar archives.
author | Joerg Sonnenberger <joerg@bec.de> |
---|---|
date | Wed, 15 Nov 2023 22:11:34 +0100 |
parents | 55d45d0de4e7 |
children | b60f25f00e94 |
line wrap: on
line diff
--- a/mercurial/archival.py Thu Sep 05 13:37:24 2024 +0200 +++ b/mercurial/archival.py Wed Nov 15 22:11:34 2023 +0100 @@ -293,8 +293,9 @@ ): """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. + 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. kind is type of archive to create. @@ -316,7 +317,37 @@ else: prefix = tidyprefix(dest, kind, prefix) + 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 + def write(name, mode, islink, getdata): + if archiver is None: + opencallback() + assert archiver is not None, "archive should be opened by now" + data = getdata() if decode: data = repo.wwritedata(name, data) @@ -325,17 +356,9 @@ if kind not in archivers: raise error.Abort(_(b"unknown archive type '%s'") % kind) - ctx = repo[node] - archiver = archivers[kind](dest, mtime or ctx.date()[0]) - if not match: match = scmutil.matchall(repo) - if repo.ui.configbool(b"ui", b"archivemeta"): - name = b'.hg_archival.txt' - if match(name): - write(name, 0o644, False, lambda: buildmetadata(ctx)) - files = list(ctx.manifest().walk(match)) total = len(files) if total: @@ -358,10 +381,11 @@ sub = ctx.workingsub(subpath) submatch = matchmod.subdirmatcher(subpath, match) subprefix = prefix + subpath + b'/' - total += sub.archive(archiver, subprefix, submatch, decode) + total += sub.archive(opencallback, subprefix, submatch, decode) if total == 0: raise error.Abort(_(b'no files match the archive pattern')) + assert archiver is not None, "archive should have been opened before" archiver.done() return total