diff --git a/mercurial/debugcommands.py b/mercurial/debugcommands.py --- a/mercurial/debugcommands.py +++ b/mercurial/debugcommands.py @@ -11,6 +11,7 @@ import collections import difflib import errno +import glob import operator import os import platform @@ -38,6 +39,7 @@ ) from . import ( bundle2, + bundlerepo, changegroup, cmdutil, color, @@ -3402,6 +3404,138 @@ @command( + b"debugbackupbundle", + [ + ( + b"", + b"recover", + b"", + b"brings the specified changeset back into the repository", + ) + ] + + cmdutil.logopts, + _(b"hg debugbackupbundle [--recover HASH]"), +) +def debugbackupbundle(ui, repo, *pats, **opts): + """lists the changesets available in backup bundles + + Without any arguments, this command prints a list of the changesets in each + backup bundle. + + --recover takes a changeset hash and unbundles the first bundle that + contains that hash, which puts that changeset back in your repository. + + --verbose will print the entire commit message and the bundle path for that + backup. + """ + backups = filter( + os.path.isfile, glob.glob(repo.vfs.join("strip-backup") + "/*.hg") + ) + backups.sort(key=lambda x: os.path.getmtime(x), reverse=True) + + opts["bundle"] = "" + opts["force"] = None + limit = logcmdutil.getlimit(opts) + + def display(other, chlist, displayer): + if opts.get("newest_first"): + chlist.reverse() + count = 0 + for n in chlist: + if limit is not None and count >= limit: + break + parents = [True for p in other.changelog.parents(n) if p != nullid] + if opts.get("no_merges") and len(parents) == 2: + continue + count += 1 + displayer.show(other[n]) + + recovernode = opts.get("recover") + if recovernode: + if scmutil.isrevsymbol(repo, recovernode): + ui.warn(_("%s already exists in the repo\n") % recovernode) + return + elif backups: + msg = _( + "Recover changesets using: hg debugbackupbundle --recover " + "\n\nAvailable backup changesets:" + ) + ui.status(msg, label="status.removed") + else: + ui.status(_("no backup changesets found\n")) + return + + for backup in backups: + # Much of this is copied from the hg incoming logic + source = ui.expandpath(os.path.relpath(backup, encoding.getcwd())) + source, branches = hg.parseurl(source, opts.get("branch")) + try: + other = hg.peer(repo, opts, source) + except error.LookupError as ex: + msg = _("\nwarning: unable to open bundle %s") % source + hint = _("\n(missing parent rev %s)\n") % short(ex.name) + ui.warn(msg, hint=hint) + continue + revs, checkout = hg.addbranchrevs( + repo, other, branches, opts.get("rev") + ) + + if revs: + revs = [other.lookup(rev) for rev in revs] + + quiet = ui.quiet + try: + ui.quiet = True + other, chlist, cleanupfn = bundlerepo.getremotechanges( + ui, repo, other, revs, opts["bundle"], opts["force"] + ) + except error.LookupError: + continue + finally: + ui.quiet = quiet + + try: + if not chlist: + continue + if recovernode: + with repo.lock(), repo.transaction("unbundle") as tr: + if scmutil.isrevsymbol(other, recovernode): + ui.status(_("Unbundling %s\n") % (recovernode)) + f = hg.openpath(ui, source) + gen = exchange.readbundle(ui, f, source) + if isinstance(gen, bundle2.unbundle20): + bundle2.applybundle( + repo, + gen, + tr, + source="unbundle", + url="bundle:" + source, + ) + else: + gen.apply(repo, "unbundle", "bundle:" + source) + break + else: + backupdate = time.strftime( + "%a %H:%M, %Y-%m-%d", + time.localtime(os.path.getmtime(source)), + ) + ui.status("\n%s\n" % (backupdate.ljust(50))) + if ui.verbose: + ui.status("%s%s\n" % ("bundle:".ljust(13), source)) + else: + opts[ + "template" + ] = "{label('status.modified', node|short)} {desc|firstline}\n" + displayer = logcmdutil.changesetdisplayer( + ui, other, opts, False + ) + display(other, chlist, displayer) + displayer.close() + finally: + cleanupfn() + + +@command( b'debugsub', [(b'r', b'rev', b'', _(b'revision to check'), _(b'REV'))], _(b'[-r REV] [REV]'), diff --git a/tests/debugbackupbundle.t b/tests/debugbackupbundle.t new file mode 100644 --- /dev/null +++ b/tests/debugbackupbundle.t @@ -0,0 +1,39 @@ + $ cat >> $HGRCPATH << EOF + > [extensions] + > strip= + > EOF + +Setup repo + + $ hg init repo + $ cd repo + +Test backups list and recover + + $ hg debugbackupbundle + no backup changesets found + + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -l $1 + > } + $ mkcommit a + $ mkcommit b + $ hg strip . + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d2ae7f538514-2953539b-backup.hg (glob) + $ hg debugbackupbundle + Recover changesets using: hg debugbackupbundle --recover + + Available backup changesets: + * (glob) + d2ae7f538514 b + + $ hg debugbackupbundle --recover d2ae7f538514 + Unbundling d2ae7f538514 + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + new changesets d2ae7f538514 (1 drafts) diff --git a/tests/test-completion.t b/tests/test-completion.t --- a/tests/test-completion.t +++ b/tests/test-completion.t @@ -75,6 +75,7 @@ $ hg debugcomplete debug debugancestor debugapplystreamclonebundle + debugbackupbundle debugbuilddag debugbundle debugcapabilities @@ -260,6 +261,7 @@ copy: forget, after, at-rev, force, include, exclude, dry-run debugancestor: debugapplystreamclonebundle: + debugbackupbundle: recover, patch, git, limit, no-merges, stat, graph, style, template debugbuilddag: mergeable-file, overwritten-file, new-file debugbundle: all, part-type, spec debugcapabilities: diff --git a/tests/test-help.t b/tests/test-help.t --- a/tests/test-help.t +++ b/tests/test-help.t @@ -973,6 +973,8 @@ find the ancestor revision of two revisions in a given index debugapplystreamclonebundle apply a stream clone bundle file + debugbackupbundle + lists the changesets available in backup bundles debugbuilddag builds a repo with a given DAG from scratch in the current empty repo