author | Gregory Szorc <gps@mozilla.com> |
Fri, 15 Jul 2016 14:52:51 -0700 | |
changeset 11050 | f8a117fa18b4ff4ae8d7bb011b912e0c258df46c |
parent 11049 | a0f0914fa6d0d913db4eb4216dfbfe603986b41e |
child 11051 | 6bd62db11a38926b61ddca43003f85be9b8f2988 |
push id | 1676 |
push user | bmo:gps@mozilla.com |
push date | Thu, 18 May 2017 00:17:53 +0000 |
reviewers | glob |
bugs | 1288845 |
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/__init__.py @@ -0,0 +1,473 @@ +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +"""undo effects of changesets in a Mozilla-centric way + +It is common for a "bad" changeset to be introduced to a repository. When this +happens, you often want to undo its impact. There are multiple ways to do this +in Mercurial, but they often require the user to make decisions, such as how +to format a commit message or which other changesets to include as part of the +undo. This extension provides commands that undo the effects of changesets in +a consistent and well-defined manner. + +Changeset Tainting Rules +======================== + +Commands take a revset that expands to the initial set of "bad" revisions. +From there, additional revisions are brought in to the "bad" set by applying +a set of rules. We call this "tainting." + +Changesets are tainted if they meet one of the following conditions: + +* They are a descendant of a "bad" revision in the same push. +* They are a descendant of a "bad" revision that touches files touched by a + "bad" revision. +""" + +from __future__ import absolute_import + +import collections +import os + +from mercurial.i18n import _ +from mercurial.node import ( + nullid, + short, +) +from mercurial import ( + cmdutil, + context, + encoding, + error, + phases, + scmutil, + util, +) + +OUR_DIR = os.path.dirname(__file__) +execfile(os.path.join(OUR_DIR, '..', 'bootstrap.py')) + +testedwith = '4.1 4.2' +minimumhgversion = '4.1' + +cmdtable = {} +command = cmdutil.command(cmdtable) + + +def resolve_discards(ui, repo, revs, action, allow_public=False, + require_pushlog=True): + """Determine what revisions to discard and why. + + Given an initial revset to evaluate, this also verifies requested + revisions can be discarded and also taints other revisions. + + Unless ``allow_public`` is set, public changesets cannot be discarded. + + Returns a dict describing results. + """ + if not revs: + raise error.Abort(_('must give at least one argument defining ' + 'revision to drop')) + + if not len(repo): + raise error.Abort(_('cannot run on empty repos')) + + if len(repo.heads()) > 1: + raise error.Abort('cannot operate on repo with multiple heads') + + + discard_revs = scmutil.revrange(repo, revs) + + if not discard_revs: + raise error.Abort(_('could not find revisions to %s') % action, + hint=_('arguments must be revisions or revsets ' + 'that resolve to known changesets')) + + # pushid -> set(revisions) + push_discards = collections.defaultdict(set) + + pushlog = getattr(repo, 'pushlog', None) + if not pushlog: + if require_pushlog: + raise error.Abort(_('pushlog not available; refusing to continue')) + else: + ui.warn(_('(warning: pushlog not available; unable to identify ' + 'revisions in same push)\n')) + + warned = False + for rev in discard_revs: + ctx = repo[rev] + desc = ctx.description().splitlines()[0] + + if ctx.phase() == phases.public and not allow_public: + ui.warn('cannot %s public changeset: %s %s\n' % + (action, short(ctx.node()), desc)) + warned = True + + # Merges cannot be discarded easily. + # + # If you discard a merge, you need to decide what to do with the 2nd + # parent and its ancestors up to the brain point. + # + # You could leave the parent around in the repo as an extra DAG + # head. However, lots of automation and workflows assume the presence + # of a single head, so this isn't attractive. + # + # And for the case where obsolescence is in play... + # + # If a parent is public, you can't obsolete public changesets + # because they are immutable. So, we'd either have to retain the + # extra DAG head (until the merge is conducted again) or strip the + # repo. And stripping is a measure of last resort, as it can cause + # havoc for downstream consumers, including the replication system. + # + # We /could/ drop the merge if both parents contained draft changesets + # up to the branch point. But this will likely not happen because + # merges to the autoland repo will come from publishing repositories. + # The easiest thing to do is refuse to drop merges. + if len(ctx.parents()) > 1: + ui.warn('cannot %s merge: %s %s\n' % + (action, short(ctx.node()), desc)) + warned = True + + # While we're iterating, assemble map of pushes to changesets in them + # being dropped. + if pushlog: + push = pushlog.pushfromchangeset(ctx) + if push: + push_discards[push.pushid].add(ctx.node()) + + if warned: + raise error.Abort(_('cannot perform %s on selected revisions') % action) + + # Above, we collected the set of revisions that were explicitly requested + # to be discarded and the pushlog pushes associated with them. + # + # Our strategy now is to start with the oldest changeset to be discarded + # and walk towards the DAG head. For each changeset, we examine to see if + # it is to be discarded. If it is, we record attributes so we can taint + # descendants. + # + # We don't (yet) perform file merges as part of the discard. So, we + # record the set of files impacted by discarded changesets. If a + # subsequent changeset touches any of those files, we also discard it. + # + # Pushes work similarly. If we encounter a changeset that is part of a + # push that previously had a discard, we discard it. The thinking here + # is that changesets from the same push that come after a discarded + # changeset are likely impacted by the discarded changeset. While this + # may# cause some false# positives, it is better to be safe than sorry. + + # Files modified by discarded changesets. + impacted_files = set() + # Path to earliest discarded revision it was touched in. + first_file_revision = {} + + # Integer rev to reason for discard. + discarded = {} + + for ctx in repo.set('%d::', discard_revs.min()): + rev = ctx.rev() + node = ctx.node() + + # If a merge commit changed files that are impacted by a discard, we + # abort because we can't rewrite merge commits. + if len(ctx.parents()) > 1 and set(ctx.files()) & impacted_files: + raise error.Abort('merge changeset %s changed files impacted by a ' + '%s; cannot proceed' % (short(node), action), + hint='perform a traditional backout commit') + + # There are edge cases where ctx.files() may not be accurate. Compute + # the file changes from the manifest diff. This is slower but more + # accurate. + changed_files = set(ctx.manifest().diff(ctx.p1().manifest()).keys()) + + def add_discarded(reason): + discarded[rev] = reason + + if len(ctx.parents()) > 1: + raise error.Abort(_('cannot %s merge changesets') % action, + hint=_('perform a traditional backout ' + 'commit')) + + impacted_files.update(changed_files) + + for f in changed_files: + first_file_revision.setdefault(f, rev) + + if pushlog: + push = pushlog.pushfromchangeset(ctx) + if push: + push_discards[push.pushid].add(node) + + # If node is already marked for discarding, discard it. + if rev in discard_revs: + add_discarded('explicit') + continue + + # If this changeset shares a push that contains a discard and comes + # after that discard, discard it. The reasoning here is worth + # explaining. + # + # If a push contains multiple changesets, some may be good and + # others bad. We want to make forward progress where possible. If only + # the last changeset is bad, we shouldn't penalize the changesets before + # it by discarding them. + # + # However, if the first or a middle changeset is bad, all bets are off + # as to the state of the following changesets. In many scenarios, those + # subsequent changesets rely on work done by the bad one. So, the only + # safe thing to do is drop all changesets in a push coming after a bad + # one. + if pushlog: + push = pushlog.pushfromchangeset(ctx) + if push and push.pushid in push_discards: + # And the changeset comes *after* a changeset in the push that + # was dropped. + if ctx.rev() in repo.revs('%ln::', push_discards[push.pushid]): + add_discarded('push') + continue + + # If this changeset touches files impacted by a drop, we also + # drop it. + if changed_files & impacted_files: + add_discarded('files') + continue + + return { + 'discards': discarded, + 'first_file_revisions': first_file_revision, + } + + +def summarize_discards(ui, repo, discards, action): + """Print a summary of what we'll discard.""" + + reasons = { + 'explicit': _('requested explicitly'), + 'push': _('shares push with discarded changeset'), + 'files': _('changes impacted files'), + } + + for ctx in repo.set('%d::', min(discards['discards'].keys())): + rev = ctx.rev() + desc = ctx.description().splitlines()[0] + if rev in discards['discards']: + reason = discards['discards'][rev] + ui.write('will %s %s (%s): %s\n' % + (action, short(ctx.node()), reasons[reason], desc)) + else: + ui.write('will retain %s: %s\n' % (short(ctx.node()), desc)) + + +def schedule_changegroup_close_hooks(repo, tr, hook_args, nodes): + def run_hooks(): + args = hook_args.copy() + args['node'] = hook_args['node'] + repo.hook('changegroup', **args) + + for node in nodes: + args = hook_args.copy() + args['node'] = node + repo.hook('incoming', **args) + + tr.addpostclose('discard-changegroup-hooks', + lambda tr: repo._afterlock(run_hooks)) + + +class DryRunAbort(Exception): + """Represents an abort due to a dry run requested.""" + + +@command('debugimpactedrevs', [ + ('p', 'public', False, _('allow operating on public revisions')), +]) +def debugbackoutimpactedrevs(ui, repo, *revs, **opts): + """Display information about impacted revisions for backouts. + + This is used to unit test the logic for determining which revisions + and files are impacted by a backout. + """ + discards = resolve_discards(ui, repo, revs, 'discard', + allow_public=opts['public'], + require_pushlog=False) + + summarize_discards(ui, repo, discards, 'discard') + + +@command('mozbackout', [ + ('d', 'date', '', + _('record the specified date as commit date'), _('DATE')), + ('u', 'user', '', + _('record the specified user as committer'), _('USER')), + ('m', 'message', '', _('reason for backout'), _('REASON')), + ('n', 'dry-run', None, _('do not perform actions; print what would be ' + 'done')), +]) +def mozbackout(ui, repo, *revs, **opts): + """Backout changesets using Mozilla conventions. + + Revisions to be backed out are resolved from the argument list. Additional + revisions are resolved via tainting rules. + """ + date = opts.get('date') + if date: + opts['date'] = util.parsedate(date) + else: + opts['date'] = util.makedate() + + if not opts.get('message'): + if not ui.interactive(): + raise error.Abort(_('-m/--message must be given when not running ' + 'interactively')) + + while True: + opts['message'] = ui.prompt(_('reason for backout: '), default='') + if opts['message']: + break + else: + ui.warn(_('must specify a reason\n')) + + opts['user'] = opts['user'] or ui.username() + + try: + with repo.lock(): + with repo.transaction('mozbackout') as tr: + _backout(ui, repo, tr, revs, **opts) + except DryRunAbort: + pass + + +def _backout(ui, repo, tr, revs, message=None, date=None, + user=None, dry_run=False): + discards = resolve_discards(ui, repo, revs, 'discard', allow_public=True) + assert discards['discards'] + + summarize_discards(ui, repo, discards, 'discard') + + backouts = discards['discards'] + + lines = [ + 'Backed out %d changesets for %s' % (len(backouts), message), + '' + ] + + reasons = { + 'explicit': 'requested explicitly', + 'push': 'shares push', + 'files': 'files impacted', + } + + for rev in sorted(backouts): + ctx = repo[rev] + lines.append('%s (%s) %s' % ( + short(ctx.node()), + reasons[backouts[rev]], + encoding.fromlocal(ctx.description()).splitlines()[0])) + + desc = encoding.tolocal('\n'.join(lines)) + + hook_args = dict(tr.hookargs) + hook_args['source'] = 'mozbackout' + + # Semantically, the precommit hook is better. However, "commit" is + # associated with working directories, which we don't have here. Few hooks + # on hg.mozilla.org work on the commit level. So we use the changegroup + # hooks. + repo.hook('prechangegroup', **hook_args) + + # We create a changeset that reverts changes made by the set of discarded + # changesets. Unlike `hg backout`, which operates at a diff/merge level to + # basically do a reverse diff, we operate at the whole file level. We can + # do this because the discarding logic "taints" all subsequent changesets + # touching reverted files. + # + # Our new changeset says there are changes to every file impacted by the + # discards. Then, when the callback fires to get details on each impacted + # file, we return a file context as it existed in the ancestor of the + # first changeset that modified this file. + + tip = repo['tip'] + + def get_file_ctx(repo, memctx, path): + assert path in discards['first_file_revisions'] + + ctx = repo[discards['first_file_revisions'][path]] + + if len(ctx.parents()) > 1: + raise error.Abort(_('we should not have encountered a merge ' + 'for a file being changed during a backout; ' + 'this is likely a bug')) + + # If this isn't a bug, we should get the fctx for both parents, + # verify they are identical, and reuse. + + try: + fctx = ctx.p1()[path] + except KeyError: + return None + + return context.memfilectx(repo, path, fctx.data(), + islink=fctx.islink(), + isexec=fctx.isexec(), + copied=fctx.renamed(), + memctx=memctx) + + ctx = context.memctx(repo, + parents=[tip.node(), nullid], + text=desc, + files=discards['first_file_revisions'].keys(), + filectxfn=get_file_ctx, + user=user, + date=date, + extra={'branch': tip.branch() or 'default'}) + + ctx = repo[ctx.commit()] + + ui.write('created backout changeset %s\n' % (short(ctx.node()))) + + # Verify that end state of modified files matches exactly what it was + # before. This is a sanity check and could be removed if it never triggers. + for path in sorted(discards['first_file_revisions'].keys()): + fctx = repo[discards['first_file_revisions'][path]] + + try: + expected = fctx.p1()[path] + except KeyError: + expected = None + + if path in ctx: + got = ctx[path] + else: + got = None + + if expected is None or got is None: + if expected != got: + raise error.Abort(_('file mismatch for %s; please report this' + 'bug along with steps to reproduce') % path) + + continue + + # We can't straight up compare the file contexts because the nodes + # should be different since they take history into account. + if (expected.islink() != got.islink() or + expected.isexec() != got.isexec() or + expected.renamed() != got.renamed() or + expected.data() != got.data()): + raise error.Abort(_('file mismatch for %s; please report this bug ' + 'along with steps to reproduce') % path) + + tr.hookargs['node'] = ctx.hex() + hook_args['node'] = ctx.hex() + + p = lambda: tr.writepending() and repo.root or '' + repo.hook('pretxnchangegroup', throw=True, pending=p, **hook_args) + + # We're still in a transaction. Aborting should roll everything + # back. We do this after the pretxnchangegroup hooks because we want + # to get as far as possible so the user knows what to expect. + if dry_run: + ui.write('(aborting because dry run requested)\n') + raise DryRunAbort() + + schedule_changegroup_close_hooks(repo, tr, hook_args, [ctx.hex()])
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/helpers.sh @@ -0,0 +1,31 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Use a sensible user name by default. +export HGUSER='Test User <someone@example.com>' + +cat >> $HGRCPATH << EOF +[alias] +mozbackout = mozbackout -d '0 0' +shortlog = log -G -T '{rev}:{node|short} {desc|firstline}' + +[diff] +git = true +EOF + +enablemozbackout() { + cat >> $HGRCPATH << EOF +[extensions] +mozbackout = $TESTDIR/hgext/mozbackout +EOF +} + +backoutrepo() { + hg init $1 + cat >> $1/.hg/hgrc << EOF +[extensions] +mozbackout = $TESTDIR/hgext/mozbackout +pushlog = $TESTDIR/hgext/pushlog +EOF +}
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-backout-basic.t @@ -0,0 +1,66 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + $ export USER=hguser + + $ backoutrepo server + $ hg -q clone server client + $ cd client + $ touch foo + $ hg -q commit -A -m initial + $ hg -q push + recorded push in pushlog + +Seed repo with independent changesets + + $ echo file0 > file0 + $ hg -q commit -A -m commit0 + $ hg -q push + recorded push in pushlog + $ echo file1 > file1 + $ hg -q commit -A -m commit1 + $ hg -q push + recorded push in pushlog + + $ cd ../server + + $ hg shortlog + o 2:3570fcb7e2e0 commit1 + | + o 1:62529fb87da7 commit0 + | + o 0:77538e1ce4be initial + + +--dry-run no-ops + + $ hg mozbackout -m 'testing dry run' --dry-run 62529fb87da7 + will discard 62529fb87da7 (requested explicitly): commit0 + will retain 3570fcb7e2e0: commit1 + created backout changeset 41206f9d36f2 + (aborting because dry run requested) + transaction abort! + rollback completed + +Backing out a commit independent of others works + + $ hg mozbackout -m 'testing independent' 62529fb87da7 + will discard 62529fb87da7 (requested explicitly): commit0 + will retain 3570fcb7e2e0: commit1 + created backout changeset d53e82cc1ca8 + + $ hg export 3 + # HG changeset patch + # User Test User <someone@example.com> + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID d53e82cc1ca807f4f0cecc5d9eb3065c2ae4d579 + # Parent 3570fcb7e2e0a37e7f23e1ec893cbb875b4f9c48 + Backed out 1 changesets for testing independent + + 62529fb87da7 (requested explicitly) commit0 + + diff --git a/file0 b/file0 + deleted file mode 100644 + --- a/file0 + +++ /dev/null + @@ -1,1 +0,0 @@ + -file0
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-backout-copy-rename.t @@ -0,0 +1,163 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + +Backing out a copy should delete the copy + + $ backoutrepo test-copy + $ cd test-copy + + $ touch foo + $ hg -q commit -A -m initial + $ echo 0 > file-orig + $ hg -q commit -A -m orig + $ hg cp file-orig file-copied + $ hg commit -m copied + $ echo 1 > file-copied + $ hg commit -m 'copied 1' + + $ hg shortlog + @ 3:a58ffe569151 copied 1 + | + o 2:0f9b20a829f6 copied + | + o 1:db5f00ccdf2a orig + | + o 0:77538e1ce4be initial + + + $ hg mozbackout 2 -m 'undo copy' + will discard 0f9b20a829f6 (requested explicitly): copied + will discard a58ffe569151 (changes impacted files): copied 1 + created backout changeset 833b30e7ec2e + + $ hg export 4 + # HG changeset patch + # User Test User <someone@example.com> + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 833b30e7ec2e3a29c8d0c488cf45098575984a76 + # Parent a58ffe56915162412728ede4aa19316fbbf76f7a + Backed out 2 changesets for undo copy + + 0f9b20a829f6 (requested explicitly) copied + a58ffe569151 (files impacted) copied 1 + + diff --git a/file-copied b/file-copied + deleted file mode 100644 + --- a/file-copied + +++ /dev/null + @@ -1,1 +0,0 @@ + -1 + +Original file should still exist after backout + + $ hg cat -r 4 file-orig + 0 + + $ cd .. + +Now back out a rename + + $ backoutrepo test-rename + $ cd test-rename + $ touch foo + $ hg -q commit -A -m initial + $ echo 0 > file-orig + $ hg -q commit -A -m orig + $ hg mv file-orig file-renamed + $ hg commit -m renamed + $ echo 1 > file-renamed + $ hg commit -m 'renamed 1' + + $ hg shortlog + @ 3:4a19ab819bc8 renamed 1 + | + o 2:dbdca74fa29b renamed + | + o 1:db5f00ccdf2a orig + | + o 0:77538e1ce4be initial + + $ hg mozbackout -m 'undo rename' 2 + will discard dbdca74fa29b (requested explicitly): renamed + will discard 4a19ab819bc8 (changes impacted files): renamed 1 + created backout changeset 7bd5c0ce655f + + $ hg export 4 + # HG changeset patch + # User Test User <someone@example.com> + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 7bd5c0ce655f627e359de4eeae69878f153d490a + # Parent 4a19ab819bc8331fdca0d4b48e8d43c7ba93efa2 + Backed out 2 changesets for undo rename + + dbdca74fa29b (requested explicitly) renamed + 4a19ab819bc8 (files impacted) renamed 1 + + diff --git a/file-orig b/file-orig + new file mode 100644 + --- /dev/null + +++ b/file-orig + @@ -0,0 +1,1 @@ + +0 + diff --git a/file-renamed b/file-renamed + deleted file mode 100644 + --- a/file-renamed + +++ /dev/null + @@ -1,1 +0,0 @@ + -1 + + $ cd .. + +Now do a rename variation but with file history, executable bit, and modification during rename + + $ backoutrepo rename-history + $ cd rename-history + $ touch foo + $ hg -q commit -A -m initial + $ echo 0 > file-orig + $ chmod +x file-orig + $ hg -q commit -A -m orig + $ echo 1 > file-orig + $ hg commit -m '0 to 1' + $ hg mv file-orig file-renamed + $ echo 2 > file-renamed + $ hg commit -m 'rename and 2 to 1' + + $ hg shortlog + @ 3:3d0ff821c59a rename and 2 to 1 + | + o 2:2d8e968233b2 0 to 1 + | + o 1:8ce60d3f3346 orig + | + o 0:77538e1ce4be initial + + + $ hg mozbackout -m 'undo rename' 3 + will discard 3d0ff821c59a (requested explicitly): rename and 2 to 1 + created backout changeset 5c52048d275d + + $ hg export 5 + # HG changeset patch + # User Test User <someone@example.com> + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 5c52048d275d6258e5b2513b023309e601cf8ee5 + # Parent 3d0ff821c59a38434bf23dca30c550e3942f5fea + Backed out 1 changesets for undo rename + + 3d0ff821c59a (requested explicitly) rename and 2 to 1 + + diff --git a/file-orig b/file-orig + new file mode 100755 + --- /dev/null + +++ b/file-orig + @@ -0,0 +1,1 @@ + +1 + diff --git a/file-renamed b/file-renamed + deleted file mode 100755 + --- a/file-renamed + +++ /dev/null + @@ -1,1 +0,0 @@ + -2
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-backout-executable-file.t @@ -0,0 +1,68 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + +Construct a repo that modifies the executable status of a file + + $ backoutrepo backout-executable + $ cd backout-executable + $ touch foo + $ hg -q commit -A -m initial + $ touch file0 + $ hg -q commit -A -m 'add file0' + $ chmod +x file0 + $ hg commit -m 'make file0 executable' + + $ hg shortlog + @ 2:d5f300b6e466 make file0 executable + | + o 1:442ce5a124e0 add file0 + | + o 0:77538e1ce4be initial + + $ hg export 2 + # HG changeset patch + # User Test User <someone@example.com> + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID d5f300b6e466623cb9b6d5d3d240d3926ba2ef99 + # Parent 442ce5a124e001862e8bd6a8871d8b85e09bebd7 + make file0 executable + + diff --git a/file0 b/file0 + old mode 100644 + new mode 100755 + +Backing out a change to an executable bit should revert the executable status + + $ hg mozbackout -m backout 2 + will discard d5f300b6e466 (requested explicitly): make file0 executable + created backout changeset 3393c206c7b7 + + $ hg files -r 2 'set:exec()' + file0 + $ hg files -r 3 'set:exec()' + [1] + + $ cd .. + +Backout out a change to an executable file should retain the executable bit + + $ backoutrepo executable-modified + $ cd executable-modified + $ touch foo + $ hg -q commit -A -m initial + $ echo 0 > file0 + $ chmod +x file0 + $ hg -q commit -A -m 'add file0' + $ echo 1 > file0 + $ hg commit -m 'modify file1' + + $ hg mozbackout -m backout 2 + will discard 0e33bfbd38b5 (requested explicitly): modify file1 + created backout changeset d9c9594b97c6 + + $ hg files -r 2 'set:exec()' + file0 + $ hg files -r 3 'set:exec()' + file0 + + $ cd ..
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-backout-prompts.t @@ -0,0 +1,53 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + + $ backoutrepo repo + $ cd repo + + $ touch foo + $ hg -q commit -A -m initial + $ echo 0 > file0 + $ hg -q commit -A -m file0 + + $ hg shortlog + @ 1:9bdfe2de4776 file0 + | + o 0:77538e1ce4be initial + + +Message must be given when not running interactively + + $ hg mozbackout 9bdfe2de4776 + abort: -m/--message must be given when not running interactively + [255] + +We prompt for message when running interactively + + $ hg --config ui.interactive=true mozbackout 9bdfe2de4776 << EOF + > + > random reason + > EOF + reason for backout: + must specify a reason + reason for backout: random reason + will discard 9bdfe2de4776 (requested explicitly): file0 + created backout changeset b668c7d8e4b5 + + $ hg export 2 + # HG changeset patch + # User Test User <someone@example.com> + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID b668c7d8e4b5c8df42775ada7986c9cb8d8ba93d + # Parent 9bdfe2de47766b5ff022d12a58a8ec61f1d36eef + Backed out 1 changesets for random reason + + 9bdfe2de4776 (requested explicitly) file0 + + diff --git a/file0 b/file0 + deleted file mode 100644 + --- a/file0 + +++ /dev/null + @@ -1,1 +0,0 @@ + -0 + + $ cd ..
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-backout-pushlog.t @@ -0,0 +1,47 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + $ export USER=hguser + + $ backoutrepo server + $ hg -q clone server client + $ cd client + $ touch foo + $ hg -q commit -A -m initial + $ hg -q push + recorded push in pushlog + +Seed repo with data + + $ echo file0 > file0 + $ hg -q commit -A -m commit0 + $ hg -q push + recorded push in pushlog + $ echo file1 > file1 + $ hg -q commit -A -m commit1 + $ hg -q push + recorded push in pushlog + + $ cd ../server + + $ hg shortlog + o 2:3570fcb7e2e0 commit1 + | + o 1:62529fb87da7 commit0 + | + o 0:77538e1ce4be initial + + +Back out tip most changesets + + $ hg mozbackout -m 'testing pushlog' 62529fb87da7:: + will discard 62529fb87da7 (requested explicitly): commit0 + will discard 3570fcb7e2e0 (requested explicitly): commit1 + created backout changeset f2bb38dd4877 + +Pushlog should be retained for original changesets +New pushlog entry should exist for backout changeset + + $ hg log -T '{rev}:{node|short} {desc|firstline} {pushid} {pushuser}\n' + 3:f2bb38dd4877 Backed out 2 changesets for testing pushlog + 2:3570fcb7e2e0 commit1 3 hguser + 1:62529fb87da7 commit0 2 hguser + 0:77538e1ce4be initial 1 hguser
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-backout-user-and-date.t @@ -0,0 +1,31 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + + $ backoutrepo repo + $ cd repo + + $ touch foo + $ hg -q commit -A -m initial + $ echo 0 > file0 + $ hg -q commit -A -m file0 + + $ hg mozbackout -u 'CLI User <cli@example.com>' -d '1495054549 28800' -m 'testing arguments' 1 + will discard 9bdfe2de4776 (requested explicitly): file0 + created backout changeset dfdf0dbb5117 + + $ hg export 2 + # HG changeset patch + # User CLI User <cli@example.com> + # Date 1495054549 28800 + # Wed May 17 12:55:49 2017 -0800 + # Node ID dfdf0dbb51171ce020350262e3cc5ea6107447ab + # Parent 9bdfe2de47766b5ff022d12a58a8ec61f1d36eef + Backed out 1 changesets for testing arguments + + 9bdfe2de4776 (requested explicitly) file0 + + diff --git a/file0 b/file0 + deleted file mode 100644 + --- a/file0 + +++ /dev/null + @@ -1,1 +0,0 @@ + -0
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-impacted-files.t @@ -0,0 +1,152 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + $ enablemozbackout + + $ hg init simple + $ cd simple + + $ touch foo + $ hg -q commit -A -m initial + $ hg phase --public -r . + $ echo 0 > file0 + $ hg -q commit -A -m 'file0 0' + $ echo 0 > file1 + $ hg -q commit -A -m 'file1 0' + $ echo 1 > file0 + $ hg commit -m 'file0 1' + $ echo 1 > file1 + $ hg commit -m 'file1 1' + $ echo 2 > file0 + $ echo 2 > file1 + $ hg commit -m 'file0/file1 2' + + $ hg shortlog + @ 5:f96c11382754 file0/file1 2 + | + o 4:2db0e84fc3d7 file1 1 + | + o 3:ab1d7521154c file0 1 + | + o 2:83bb29ac0b25 file1 0 + | + o 1:7a649de10bd2 file0 0 + | + o 0:77538e1ce4be initial + + +Backing out a changeset touching file X will back out descendants touching that file + + $ hg debugimpactedrevs 7a649de10bd2 + (warning: pushlog not available; unable to identify revisions in same push) + will discard 7a649de10bd2 (requested explicitly): file0 0 + will retain 83bb29ac0b25: file1 0 + will discard ab1d7521154c (changes impacted files): file0 1 + will retain 2db0e84fc3d7: file1 1 + will discard f96c11382754 (changes impacted files): file0/file1 2 + +File not modified by descendants should not result in tainting + + $ hg debugimpactedrevs --public 77538e1ce4be + (warning: pushlog not available; unable to identify revisions in same push) + will discard 77538e1ce4be (requested explicitly): initial + will retain 7a649de10bd2: file0 0 + will retain 83bb29ac0b25: file1 0 + will retain ab1d7521154c: file0 1 + will retain 2db0e84fc3d7: file1 1 + will retain f96c11382754: file0/file1 2 + + $ cd .. + +Now test multiple files being modified in a changeset + + $ hg init multiple-files + $ cd multiple-files + $ touch foo + $ hg -q commit -A -m initial + + $ echo 0 > file0 + $ hg -q commit -A -m 'file0 0' + $ echo 1 > file0 + $ echo 0 > file1 + $ hg -q commit -A -m 'file0 1 file1 0' + $ echo 2 > file0 + $ hg commit -m 'file0 2' + $ echo 1 > file1 + $ hg commit -m 'file1 1' + $ echo 0 > file2 + $ hg -q commit -A -m 'file2 0' + + $ hg shortlog + @ 5:72fb263e36ff file2 0 + | + o 4:90b2e91ede59 file1 1 + | + o 3:7240bafd09a7 file0 2 + | + o 2:62d0909acd7e file0 1 file1 0 + | + o 1:7a649de10bd2 file0 0 + | + o 0:77538e1ce4be initial + + + $ hg debugimpactedrevs 62d0909acd7e + (warning: pushlog not available; unable to identify revisions in same push) + will discard 62d0909acd7e (requested explicitly): file0 1 file1 0 + will discard 7240bafd09a7 (changes impacted files): file0 2 + will discard 90b2e91ede59 (changes impacted files): file1 1 + will retain 72fb263e36ff: file2 0 + + $ cd .. + +Files modified by tainted changesets should taint subsequent changesets + + $ hg init taint-chaining + $ cd taint-chaining + $ touch foo + $ hg -q commit -A -m initial + $ echo 0 > file0 + $ hg -q commit -A -m 'file0 0' + $ echo 1 > file0 + $ echo 0 > file1 + $ hg -q commit -A -m 'file0 1 file1 0' + $ echo 2 > file0 + $ echo 1 > file1 + $ echo 0 > file2 + $ hg -q commit -A -m 'file0 2 file1 1 file2 0' + $ echo 3 > file0 + $ hg commit -m 'file0 3' + $ echo 2 > file1 + $ hg commit -m 'file1 2' + $ echo 1 > file2 + $ hg commit -m 'file2 1' + $ echo 0 > file3 + $ hg -q commit -A -m 'file3 0' + + $ hg shortlog + @ 7:8048f884a24e file3 0 + | + o 6:b08a9de38526 file2 1 + | + o 5:ff9bd664ee33 file1 2 + | + o 4:e219eba74228 file0 3 + | + o 3:2a0ac94cd7fd file0 2 file1 1 file2 0 + | + o 2:62d0909acd7e file0 1 file1 0 + | + o 1:7a649de10bd2 file0 0 + | + o 0:77538e1ce4be initial + + $ hg debugimpactedrevs 7a649de10bd2 + (warning: pushlog not available; unable to identify revisions in same push) + will discard 7a649de10bd2 (requested explicitly): file0 0 + will discard 62d0909acd7e (changes impacted files): file0 1 file1 0 + will discard 2a0ac94cd7fd (changes impacted files): file0 2 file1 1 file2 0 + will discard e219eba74228 (changes impacted files): file0 3 + will discard ff9bd664ee33 (changes impacted files): file1 2 + will discard b08a9de38526 (changes impacted files): file2 1 + will retain 8048f884a24e: file3 0 + + $ cd ..
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-impacted-merge.t @@ -0,0 +1,71 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + $ enablemozbackout + + $ hg init repo + $ cd repo + + $ touch foo + $ hg -q commit -A -m initial + $ hg phase --public -r . + $ echo 0 > file0 + $ hg -q commit -A -m 'file0 0' + $ echo 0 > file1 + $ hg -q commit -A -m 'file1 0' + $ echo 1a > file0 + $ hg commit -m 'file0 1a' + $ hg -q up 2 + $ echo 1b > file0 + $ hg commit -m 'file0 1b' + created new head + $ echo 2 > file0 + $ hg commit -m 'file0 2' + $ hg -q up 3 + + $ hg merge -t :other tip + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg commit -m merge + + $ hg shortlog + @ 6:2a969f69b3c8 merge + |\ + | o 5:c59306c59a8f file0 2 + | | + | o 4:431d61777bf6 file0 1b + | | + o | 3:8aab5419c2fc file0 1a + |/ + o 2:83bb29ac0b25 file1 0 + | + o 1:7a649de10bd2 file0 0 + | + o 0:77538e1ce4be initial + + +Merges cannot be specifically targeted for discard + + $ hg debugimpactedrevs 2a969f69b3c8 + (warning: pushlog not available; unable to identify revisions in same push) + cannot discard merge: 2a969f69b3c8 merge + abort: cannot perform discard on selected revisions + [255] + +Merges touching files that are impacted are not be allowed + + $ hg debugimpactedrevs 431d61777bf6 + (warning: pushlog not available; unable to identify revisions in same push) + abort: merge changeset 2a969f69b3c8 changed files impacted by a discard; cannot proceed + (perform a traditional backout commit) + [255] + +Merges not touching impacted files are OK + + $ hg debugimpactedrevs 83bb29ac0b25 + (warning: pushlog not available; unable to identify revisions in same push) + will discard 83bb29ac0b25 (requested explicitly): file1 0 + will retain 8aab5419c2fc: file0 1a + will retain 431d61777bf6: file0 1b + will retain c59306c59a8f: file0 2 + will retain 2a969f69b3c8: merge + + $ cd ..
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-impacted-pushlog.t @@ -0,0 +1,123 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + $ export USER=hguser + + $ backoutrepo server + $ hg -q clone server client + $ cd client + $ touch foo + $ hg -q commit -A -m initial + $ hg phase --public -r . + $ hg -q push + recorded push in pushlog + + $ echo 0 > file0 + $ hg -q commit -A -m 'push 2 commit 1' + $ echo 1 > file1 + $ hg -q commit -A -m 'push 2 commit 2' + $ echo 2 > file2 + $ hg -q commit -A -m 'push 2 commit 3' + $ hg -q push + recorded push in pushlog + + $ echo 3 > file0 + $ hg -q commit -A -m 'push 3 commit 1' + $ echo 4 > file4 + $ hg -q commit -A -m 'push 3 commit 2' + $ echo 5 > file5 + $ hg -q commit -A -m 'push 3 commit 3' + $ hg -q push + recorded push in pushlog + + $ cd ../server + + $ hg log -G -T '{rev}:{node|short} {desc} {pushid} {pushuser}\n' + o 6:7955fa0a974c push 3 commit 3 3 hguser + | + o 5:e659d32b8f4c push 3 commit 2 3 hguser + | + o 4:54b41e41f338 push 3 commit 1 3 hguser + | + o 3:1589f1d81ece push 2 commit 3 2 hguser + | + o 2:36bfb0990e62 push 2 commit 2 2 hguser + | + o 1:5087d8fb2e3b push 2 commit 1 2 hguser + | + o 0:77538e1ce4be initial 1 hguser + + +Requesting to discard a changeset in middle of push should drop descendants in that push + + $ hg debugimpactedrevs --public 36bfb0990e62 + will discard 36bfb0990e62 (requested explicitly): push 2 commit 2 + will discard 1589f1d81ece (shares push with discarded changeset): push 2 commit 3 + will retain 54b41e41f338: push 3 commit 1 + will retain e659d32b8f4c: push 3 commit 2 + will retain 7955fa0a974c: push 3 commit 3 + +Discarding first changeset in push should discard entire push + + $ hg debugimpactedrevs --public 54b41e41f338 + will discard 54b41e41f338 (requested explicitly): push 3 commit 1 + will discard e659d32b8f4c (shares push with discarded changeset): push 3 commit 2 + will discard 7955fa0a974c (shares push with discarded changeset): push 3 commit 3 + +Discarding a changeset containing a file in a subsequent push should discard +subsequent changeset and all changesets after it in push + + $ hg debugimpactedrevs --public 5087d8fb2e3b + will discard 5087d8fb2e3b (requested explicitly): push 2 commit 1 + will discard 36bfb0990e62 (shares push with discarded changeset): push 2 commit 2 + will discard 1589f1d81ece (shares push with discarded changeset): push 2 commit 3 + will discard 54b41e41f338 (changes impacted files): push 3 commit 1 + will discard e659d32b8f4c (shares push with discarded changeset): push 3 commit 2 + will discard 7955fa0a974c (shares push with discarded changeset): push 3 commit 3 + +Tip-most changeset in a push can be discarded in isolation + + $ hg debugimpactedrevs --public 1589f1d81ece + will discard 1589f1d81ece (requested explicitly): push 2 commit 3 + will retain 54b41e41f338: push 3 commit 1 + will retain e659d32b8f4c: push 3 commit 2 + will retain 7955fa0a974c: push 3 commit 3 + + $ hg debugimpactedrevs --public 1589f1d81ece 7955fa0a974c + will discard 1589f1d81ece (requested explicitly): push 2 commit 3 + will retain 54b41e41f338: push 3 commit 1 + will retain e659d32b8f4c: push 3 commit 2 + will discard 7955fa0a974c (requested explicitly): push 3 commit 3 + +Discarding a changeset without a pushlog entry is handled properly + + $ hg -q up tip + $ echo 6 > file6 + $ hg -q commit -A -m 'no push commit 1' + $ echo 7 > file7 + $ hg -q commit -A -m 'no push commit 2' + + $ hg shortlog + @ 8:98c1495c00a3 no push commit 2 + | + o 7:271e364060fb no push commit 1 + | + o 6:7955fa0a974c push 3 commit 3 + | + o 5:e659d32b8f4c push 3 commit 2 + | + o 4:54b41e41f338 push 3 commit 1 + | + o 3:1589f1d81ece push 2 commit 3 + | + o 2:36bfb0990e62 push 2 commit 2 + | + o 1:5087d8fb2e3b push 2 commit 1 + | + o 0:77538e1ce4be initial + + + $ hg debugimpactedrevs 271e364060fb + will discard 271e364060fb (requested explicitly): no push commit 1 + will retain 98c1495c00a3: no push commit 2 + + $ hg debugimpactedrevs 98c1495c00a3 + will discard 98c1495c00a3 (requested explicitly): no push commit 2
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-impacted-repo-preconditions.t @@ -0,0 +1,29 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + $ enablemozbackout + +Cannot run on empty repo + + $ backoutrepo empty + $ cd empty + $ hg debugimpactedrevs tip + abort: cannot run on empty repos + [255] + $ cd .. + +Cannot operate on repo with multiple heads + + $ backoutrepo multiple-heads + $ cd multiple-heads + $ touch foo + $ hg -q commit -A -m initial + $ touch bar + $ hg -q commit -A -m head1 + $ hg -q up -r 0 + $ touch baz + $ hg -q commit -A -m head2 + + $ hg debugimpactedrevs tip + abort: cannot operate on repo with multiple heads + [255] + + $ cd ..
new file mode 100644 --- /dev/null +++ b/hgext/mozbackout/tests/test-impacted-revision-validation.t @@ -0,0 +1,45 @@ + $ . $TESTDIR/hgext/mozbackout/tests/helpers.sh + $ enablemozbackout + + $ hg init simple + $ cd simple + + $ touch foo + $ hg -q commit -A -m initial + $ hg phase --public -r . + $ echo 0 > file0 + $ hg -q commit -A -m 'file0 0' + $ hg phase --public -r . + + $ hg shortlog + @ 1:7a649de10bd2 file0 0 + | + o 0:77538e1ce4be initial + + +Unknown revision aborts + + $ hg debugimpactedrevs unknown + abort: unknown revision 'unknown'! + [255] + + $ hg debugimpactedrevs 42 + abort: unknown revision '42'! + [255] + + $ hg debugimpactedrevs 'author(gps)' + abort: could not find revisions to discard + (arguments must be revisions or revsets that resolve to known changesets) + [255] + +Cannot discard public changesets by default + + $ hg debugimpactedrevs 7a649de10bd2 + (warning: pushlog not available; unable to identify revisions in same push) + cannot discard public changeset: 7a649de10bd2 file0 0 + abort: cannot perform discard on selected revisions + [255] + + $ hg debugimpactedrevs --public 7a649de10bd2 + (warning: pushlog not available; unable to identify revisions in same push) + will discard 7a649de10bd2 (requested explicitly): file0 0