def unamend(ui, repo, **opts): """undo the most recent amend operation on a current changeset This command will roll back to the previous version of a changeset, leaving working directory in state in which it was before running `hg amend` (e.g. files modified as part of an amend will be marked as modified `hg status`) """ unfi = repo.unfiltered() with repo.wlock(), repo.lock(), repo.transaction(b'unamend'): # identify the commit from which to unamend curctx = repo[b'.'] rewriteutil.precheck(repo, [curctx.rev()], b'unamend') # identify the commit to which to unamend markers = list(predecessormarkers(curctx)) if len(markers) != 1: e = _( b"changeset must have one predecessor, found %i predecessors") raise error.Abort(e % len(markers)) prednode = markers[0].prednode() predctx = unfi[prednode] # add an extra so that we get a new hash # note: allowing unamend to undo an unamend is an intentional feature extras = predctx.extra() extras[b'unamend_source'] = curctx.hex() def filectxfn(repo, ctx_, path): try: return predctx.filectx(path) except KeyError: return None # Make a new commit same as predctx newctx = context.memctx( repo, parents=(predctx.p1(), predctx.p2()), text=predctx.description(), files=predctx.files(), filectxfn=filectxfn, user=predctx.user(), date=predctx.date(), extra=extras, ) newprednode = repo.commitctx(newctx) newpredctx = repo[newprednode] dirstate = repo.dirstate with dirstate.parentchange(): scmutil.movedirstate(repo, newpredctx) mapping = {curctx.node(): (newprednode, )} scmutil.cleanupnodes(repo, mapping, b'unamend', fixphase=True)
def fix(ui, repo, *pats, **opts): """rewrite file content in changesets or working directory Runs any configured tools to fix the content of files. Only affects files with changes, unless file arguments are provided. Only affects changed lines of files, unless the --whole flag is used. Some tools may always affect the whole file regardless of --whole. If revisions are specified with --rev, those revisions will be checked, and they may be replaced with new revisions that have fixed file content. It is desirable to specify all descendants of each specified revision, so that the fixes propagate to the descendants. If all descendants are fixed at the same time, no merging, rebasing, or evolution will be required. If --working-dir is used, files with uncommitted changes in the working copy will be fixed. If the checked-out revision is also fixed, the working directory will update to the replacement revision. When determining what lines of each file to fix at each revision, the whole set of revisions being fixed is considered, so that fixes to earlier revisions are not forgotten in later ones. The --base flag can be used to override this default behavior, though it is not usually desirable to do so. """ opts = pycompat.byteskwargs(opts) if opts['all']: if opts['rev']: raise error.Abort(_('cannot specify both "--rev" and "--all"')) opts['rev'] = ['not public() and not obsolete()'] opts['working_dir'] = True with repo.wlock(), repo.lock(): revstofix = getrevstofix(ui, repo, opts) basectxs = getbasectxs(repo, opts, revstofix) workqueue, numitems = getworkqueue(ui, repo, pats, opts, revstofix, basectxs) filedata = collections.defaultdict(dict) replacements = {} fixers = getfixers(ui) # Some day this loop can become a worker pool, but for now it's easier # to fix everything serially in topological order. for rev, path in sorted(workqueue): ctx = repo[rev] olddata = ctx[path].data() newdata = fixfile(ui, opts, fixers, ctx, path, basectxs[rev]) if newdata != olddata: filedata[rev][path] = newdata numitems[rev] -= 1 if not numitems[rev]: if rev == wdirrev: writeworkingdir(repo, ctx, filedata[rev], replacements) else: replacerev(ui, repo, ctx, filedata[rev], replacements) del filedata[rev] replacements = { prec: [succ] for prec, succ in replacements.iteritems() } scmutil.cleanupnodes(repo, replacements, 'fix')
def _cleanupoldcommits(self): replacements = { k: ([v] if v is not None else []) for k, v in pycompat.iteritems(self.replacemap) } if replacements: scmutil.cleanupnodes( self.repo, replacements, operation=b'absorb', fixphase=True )
def dosplit(ui, repo, tr, ctx, opts): committed = [] # [ctx] # Set working parent to ctx.p1(), and keep working copy as ctx's content if ctx.node() != repo.dirstate.p1(): hg.clean(repo, ctx.node(), show_stats=False) with repo.dirstate.parentchange(): scmutil.movedirstate(repo, ctx.p1()) # Any modified, added, removed, deleted result means split is incomplete def incomplete(repo): st = repo.status() return any((st.modified, st.added, st.removed, st.deleted)) # Main split loop while incomplete(repo): if committed: header = _(b'HG: Splitting %s. So far it has been split into:\n' ) % short(ctx.node()) # We don't want color codes in the commit message template, so # disable the label() template function while we render it. with ui.configoverride({(b'templatealias', b'label(l,x)'): b"x"}, b'split'): for c in committed: summary = cmdutil.format_changeset_summary(ui, c, b'split') header += _(b'HG: - %s\n') % summary header += _( b'HG: Write commit message for the next split changeset.\n') else: header = _(b'HG: Splitting %s. Write commit message for the ' b'first split changeset.\n') % short(ctx.node()) opts.update({ b'edit': True, b'interactive': True, b'message': header + ctx.description(), }) commands.commit(ui, repo, **pycompat.strkwargs(opts)) newctx = repo[b'.'] committed.append(newctx) if not committed: raise error.InputError(_(b'cannot split an empty revision')) scmutil.cleanupnodes( repo, {ctx.node(): [c.node() for c in committed]}, operation=b'split', fixphase=True, ) return committed[-1]
def cleanup(repo, replacements, wdirwritten): """Calls scmutil.cleanupnodes() with the given replacements. "replacements" is a dict from nodeid to nodeid, with one key and one value for every revision that was affected by fixing. This is slightly different from cleanupnodes(). "wdirwritten" is a bool which tells whether the working copy was affected by fixing, since it has no entry in "replacements". Useful as a hook point for extending "hg fix" with output summarizing the effects of the command, though we choose not to output anything here. """ replacements = {prec: [succ] for prec, succ in replacements.iteritems()} scmutil.cleanupnodes(repo, replacements, 'fix', fixphase=True)
def uncommit(ui, repo, *pats, **opts): """uncommit part or all of a local changeset This command undoes the effect of a local commit, returning the affected files to their uncommitted state. This means that files modified or deleted in the changeset will be left unchanged, and so will remain modified in the working directory. """ with repo.wlock(), repo.lock(): wctx = repo[None] if not pats and not repo.ui.configbool('experimental', 'uncommitondirtywdir'): cmdutil.bailifchanged(repo) if wctx.parents()[0].node() == node.nullid: raise error.Abort(_("cannot uncommit null changeset")) if len(wctx.parents()) > 1: raise error.Abort(_("cannot uncommit while merging")) old = repo['.'] if not old.mutable(): raise error.Abort(_('cannot uncommit public changesets')) if len(old.parents()) > 1: raise error.Abort(_("cannot uncommit merge changeset")) allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt) if not allowunstable and old.children(): raise error.Abort(_('cannot uncommit changeset with children')) with repo.transaction('uncommit'): match = scmutil.match(old, pats, opts) newid = _commitfiltered(repo, old, match, opts.get('keep')) if newid is None: ui.status(_("nothing to uncommit\n")) return 1 mapping = {} if newid != old.p1().node(): # Move local changes on filtered changeset mapping[old.node()] = (newid, ) else: # Fully removed the old commit mapping[old.node()] = () scmutil.cleanupnodes(repo, mapping, 'uncommit') with repo.dirstate.parentchange(): repo.dirstate.setparents(newid, node.nullid) _uncommitdirstate(repo, old, match)
def uncommit(ui, repo, *pats, **opts): """uncommit part or all of a local changeset This command undoes the effect of a local commit, returning the affected files to their uncommitted state. This means that files modified or deleted in the changeset will be left unchanged, and so will remain modified in the working directory. If no files are specified, the commit will be pruned, unless --keep is given. """ opts = pycompat.byteskwargs(opts) with repo.wlock(), repo.lock(): if not pats and not repo.ui.configbool('experimental', 'uncommitondirtywdir'): cmdutil.bailifchanged(repo) old = repo['.'] rewriteutil.precheck(repo, [old.rev()], 'uncommit') if len(old.parents()) > 1: raise error.Abort(_("cannot uncommit merge changeset")) with repo.transaction('uncommit'): match = scmutil.match(old, pats, opts) keepcommit = opts.get('keep') or pats newid = _commitfiltered(repo, old, match, keepcommit) if newid is None: ui.status(_("nothing to uncommit\n")) return 1 mapping = {} if newid != old.p1().node(): # Move local changes on filtered changeset mapping[old.node()] = (newid, ) else: # Fully removed the old commit mapping[old.node()] = () scmutil.cleanupnodes(repo, mapping, 'uncommit') with repo.dirstate.parentchange(): repo.dirstate.setparents(newid, node.nullid) s = repo.status(old.p1(), old, match=match) _fixdirstate(repo, old, repo[newid], s)
def dosplit(ui, repo, tr, ctx, opts): committed = [] # [ctx] # Set working parent to ctx.p1(), and keep working copy as ctx's content if ctx.node() != repo.dirstate.p1(): hg.clean(repo, ctx.node(), show_stats=False) with repo.dirstate.parentchange(): scmutil.movedirstate(repo, ctx.p1()) # Any modified, added, removed, deleted result means split is incomplete incomplete = lambda repo: any(repo.status()[:4]) # Main split loop while incomplete(repo): if committed: header = _(b'HG: Splitting %s. So far it has been split into:\n' ) % short(ctx.node()) for c in committed: firstline = c.description().split(b'\n', 1)[0] header += _(b'HG: - %s: %s\n') % (short(c.node()), firstline) header += _( b'HG: Write commit message for the next split changeset.\n') else: header = _(b'HG: Splitting %s. Write commit message for the ' b'first split changeset.\n') % short(ctx.node()) opts.update({ b'edit': True, b'interactive': True, b'message': header + ctx.description(), }) commands.commit(ui, repo, **pycompat.strkwargs(opts)) newctx = repo[b'.'] committed.append(newctx) if not committed: raise error.Abort(_(b'cannot split an empty revision')) scmutil.cleanupnodes( repo, {ctx.node(): [c.node() for c in committed]}, operation=b'split', fixphase=True, ) return committed[-1]
def dosplit(ui, repo, tr, ctx, opts): committed = [] # [ctx] # Set working parent to ctx.p1(), and keep working copy as ctx's content # NOTE: if we can have "update without touching working copy" API, the # revert step could be cheaper. hg.clean(repo, ctx.p1().node(), show_stats=False) parents = repo.changelog.parents(ctx.node()) ui.pushbuffer() cmdutil.revert(ui, repo, ctx, parents) ui.popbuffer() # discard "reverting ..." messages # Any modified, added, removed, deleted result means split is incomplete incomplete = lambda repo: any(repo.status()[:4]) # Main split loop while incomplete(repo): if committed: header = (_('HG: Splitting %s. So far it has been split into:\n') % short(ctx.node())) for c in committed: firstline = c.description().split('\n', 1)[0] header += _('HG: - %s: %s\n') % (short(c.node()), firstline) header += _('HG: Write commit message for the next split ' 'changeset.\n') else: header = _('HG: Splitting %s. Write commit message for the ' 'first split changeset.\n') % short(ctx.node()) opts.update({ 'edit': True, 'interactive': True, 'message': header + ctx.description(), }) commands.commit(ui, repo, **pycompat.strkwargs(opts)) newctx = repo['.'] committed.append(newctx) if not committed: raise error.Abort(_('cannot split an empty revision')) scmutil.cleanupnodes(repo, {ctx.node(): [c.node() for c in committed]}, operation='split', fixphase=True) return committed[-1]
def phabsend(ui, repo, *revs, **opts): """upload changesets to Phabricator If there are multiple revisions specified, they will be send as a stack with a linear dependencies relationship using the order specified by the revset. For the first time uploading changesets, local tags will be created to maintain the association. After the first time, phabsend will check obsstore and tags information so it can figure out whether to update an existing Differential Revision, or create a new one. If --amend is set, update commit messages so they have the ``Differential Revision`` URL, remove related tags. This is similar to what arcanist will do, and is more desired in author-push workflows. Otherwise, use local tags to record the ``Differential Revision`` association. The --confirm option lets you confirm changesets before sending them. You can also add following to your configuration file to make it default behaviour:: [phabsend] confirm = true phabsend will check obsstore and the above association to decide whether to update an existing Differential Revision, or create a new one. """ revs = list(revs) + opts.get(b'rev', []) revs = scmutil.revrange(repo, revs) if not revs: raise error.Abort(_(b'phabsend requires at least one changeset')) if opts.get(b'amend'): cmdutil.checkunfinished(repo) # {newnode: (oldnode, olddiff, olddrev} oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs]) confirm = ui.configbool(b'phabsend', b'confirm') confirm |= bool(opts.get(b'confirm')) if confirm: confirmed = _confirmbeforesend(repo, revs, oldmap) if not confirmed: raise error.Abort(_(b'phabsend cancelled')) actions = [] reviewers = opts.get(b'reviewer', []) if reviewers: phids = user_group_phids(repo, reviewers) actions.append({b'type': b'reviewers.add', b'value': phids}) drevids = [] # [int] diffmap = {} # {newnode: diff} # Send patches one by one so we know their Differential Revision IDs and # can provide dependency relationship lastrevid = None for rev in revs: ui.debug(b'sending rev %d\n' % rev) ctx = repo[rev] acts = list(actions) reviewers = list(commitparser.parse_reviewers(ctx.description())) if reviewers: phids = user_group_phids(repo, reviewers) acts.append({b'type': b'reviewers.add', b'value': phids}) bugs = commitparser.parse_bugs(ctx.description()) if bugs: acts.append({b'type': b'bugzilla.bug-id', b'value': str(bugs[0]).encode('utf-8')}) # Get Differential Revision ID oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None)) if oldnode != ctx.node() or opts.get(b'amend'): # Create or update Differential Revision revision, diff = createdifferentialrevision( ctx, revid, lastrevid, oldnode, olddiff, acts) diffmap[ctx.node()] = diff newrevid = int(revision[r'object'][r'id']) if revid: action = b'updated' else: action = b'created' # Create a local tag to note the association, if commit message # does not have it already m = _differentialrevisiondescre.search(ctx.description()) if not m or int(m.group(b'id')) != newrevid: tagname = b'D%d' % newrevid tags.tag(repo, tagname, ctx.node(), message=None, user=None, date=None, local=True) else: # Nothing changed. But still set "newrevid" so the next revision # could depend on this one. newrevid = revid action = b'skipped' actiondesc = ui.label( {b'created': _(b'created'), b'skipped': _(b'skipped'), b'updated': _(b'updated')}[action], b'phabricator.action.%s' % action) drevdesc = ui.label(b'D%s' % newrevid, b'phabricator.drev') nodedesc = ui.label(bytes(ctx), b'phabricator.node') desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc') ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc)) drevids.append(newrevid) lastrevid = newrevid # Update commit messages and remove tags if opts.get(b'amend'): unfi = repo.unfiltered() drevs = callconduit(repo, b'differential.query', {b'ids': drevids}) with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'): wnode = unfi[b'.'].node() mapping = {} # {oldnode: [newnode]} for i, rev in enumerate(revs): old = unfi[rev] drevid = drevids[i] drev = [d for d in drevs if int(d[r'id']) == drevid][0] newdesc = getdescfromdrev(drev) # Make sure commit message contain "Differential Revision" if old.description() != newdesc: parents = [ mapping.get(old.p1().node(), (old.p1(),))[0], mapping.get(old.p2().node(), (old.p2(),))[0], ] new = context.metadataonlyctx( repo, old, parents=parents, text=newdesc, user=old.user(), date=old.date(), extra=old.extra()) newnode = new.commit() mapping[old.node()] = [newnode] # Update diff property writediffproperties(unfi[newnode], diffmap[old.node()]) # Remove local tags since it's no longer necessary tagname = b'D%d' % drevid if tagname in repo.tags(): tags.tag(repo, tagname, nullid, message=None, user=None, date=None, local=True) scmutil.cleanupnodes(repo, mapping, b'phabsend') if wnode in mapping: unfi.setparents(mapping[wnode][0])
def phabsend(ui, repo, *revs, **opts): """upload changesets to Phabricator If there are multiple revisions specified, they will be send as a stack with a linear dependencies relationship using the order specified by the revset. For the first time uploading changesets, local tags will be created to maintain the association. After the first time, phabsend will check obsstore and tags information so it can figure out whether to update an existing Differential Revision, or create a new one. If --amend is set, update commit messages so they have the ``Differential Revision`` URL, remove related tags. This is similar to what arcanist will do, and is more desired in author-push workflows. Otherwise, use local tags to record the ``Differential Revision`` association. The --confirm option lets you confirm changesets before sending them. You can also add following to your configuration file to make it default behaviour:: [phabsend] confirm = true phabsend will check obsstore and the above association to decide whether to update an existing Differential Revision, or create a new one. """ revs = list(revs) + opts.get(b'rev', []) revs = scmutil.revrange(repo, revs) if not revs: raise error.Abort(_(b'phabsend requires at least one changeset')) if opts.get(b'amend'): cmdutil.checkunfinished(repo) # {newnode: (oldnode, olddiff, olddrev} oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs]) confirm = ui.configbool(b'phabsend', b'confirm') confirm |= bool(opts.get(b'confirm')) if confirm: confirmed = _confirmbeforesend(repo, revs, oldmap) if not confirmed: raise error.Abort(_(b'phabsend cancelled')) actions = [] reviewers = opts.get(b'reviewer', []) if reviewers: phids = userphids(repo, reviewers) actions.append({b'type': b'reviewers.add', b'value': phids}) drevids = [] # [int] diffmap = {} # {newnode: diff} # Send patches one by one so we know their Differential Revision IDs and # can provide dependency relationship lastrevid = None for rev in revs: ui.debug(b'sending rev %d\n' % rev) ctx = repo[rev] # Get Differential Revision ID oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None)) if oldnode != ctx.node() or opts.get(b'amend'): # Create or update Differential Revision revision, diff = createdifferentialrevision( ctx, revid, lastrevid, oldnode, olddiff, actions) diffmap[ctx.node()] = diff newrevid = int(revision[r'object'][r'id']) if revid: action = b'updated' else: action = b'created' # Create a local tag to note the association, if commit message # does not have it already m = _differentialrevisiondescre.search(ctx.description()) if not m or int(m.group(b'id')) != newrevid: tagname = b'D%d' % newrevid tags.tag(repo, tagname, ctx.node(), message=None, user=None, date=None, local=True) else: # Nothing changed. But still set "newrevid" so the next revision # could depend on this one. newrevid = revid action = b'skipped' actiondesc = ui.label( {b'created': _(b'created'), b'skipped': _(b'skipped'), b'updated': _(b'updated')}[action], b'phabricator.action.%s' % action) drevdesc = ui.label(b'D%s' % newrevid, b'phabricator.drev') nodedesc = ui.label(bytes(ctx), b'phabricator.node') desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc') ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc)) drevids.append(newrevid) lastrevid = newrevid # Update commit messages and remove tags if opts.get(b'amend'): unfi = repo.unfiltered() drevs = callconduit(repo, b'differential.query', {b'ids': drevids}) with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'): wnode = unfi[b'.'].node() mapping = {} # {oldnode: [newnode]} for i, rev in enumerate(revs): old = unfi[rev] drevid = drevids[i] drev = [d for d in drevs if int(d[r'id']) == drevid][0] newdesc = getdescfromdrev(drev) newdesc = encoding.unitolocal(newdesc) # Make sure commit message contain "Differential Revision" if old.description() != newdesc: parents = [ mapping.get(old.p1().node(), (old.p1(),))[0], mapping.get(old.p2().node(), (old.p2(),))[0], ] new = context.metadataonlyctx( repo, old, parents=parents, text=newdesc, user=old.user(), date=old.date(), extra=old.extra()) newnode = new.commit() mapping[old.node()] = [newnode] # Update diff property writediffproperties(unfi[newnode], diffmap[old.node()]) # Remove local tags since it's no longer necessary tagname = b'D%d' % drevid if tagname in repo.tags(): tags.tag(repo, tagname, nullid, message=None, user=None, date=None, local=True) scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True) if wnode in mapping: unfi.setparents(mapping[wnode][0])
def fold(ui, repo, *revs, **opts): """fold multiple revisions into a single one With --from, folds all the revisions linearly between the given revisions and the parent of the working directory. With --exact, folds only the specified revisions while ignoring the parent of the working directory. In this case, the given revisions must form a linear unbroken chain. .. container:: verbose Some examples: - Fold the current revision with its parent:: hg fold --from .^ - Fold all draft revisions with working directory parent:: hg fold --from 'draft()' See :hg:`help phases` for more about draft revisions and :hg:`help revsets` for more about the `draft()` keyword - Fold revisions between 3 and 6 with the working directory parent:: hg fold --from 3::6 - Fold revisions 3 and 4: hg fold "3 + 4" --exact - Only fold revisions linearly between foo and @:: hg fold foo::@ --exact """ revs = list(revs) revs.extend(opts['rev']) if not revs: raise error.Abort(_('no revisions specified')) revs = scmutil.revrange(repo, revs) if opts.get('no_rebase'): torebase = () else: torebase = repo.revs('descendants(%ld) - (%ld)', revs, revs) if opts['from'] and opts['exact']: raise error.Abort(_('cannot use both --from and --exact')) elif opts['from']: # Try to extend given revision starting from the working directory extrevs = repo.revs('(%ld::.) or (.::%ld)', revs, revs) discardedrevs = [r for r in revs if r not in extrevs] if discardedrevs: msg = _("cannot fold non-linear revisions") hint = _("given revisions are unrelated to parent of working" " directory") raise error.Abort(msg, hint=hint) revs = extrevs elif opts['exact']: # Nothing to do; "revs" is already set correctly pass else: raise error.Abort(_('must specify either --from or --exact')) if not revs: raise error.Abort(_('specified revisions evaluate to an empty set'), hint=_('use different revision arguments')) elif len(revs) == 1: ui.write_err(_('single revision specified, nothing to fold\n')) return 1 with repo.wlock(), repo.lock(), ui.formatter('fold', opts) as fm: fm.startitem() root, head = _foldcheck(repo, revs) with repo.transaction('fold') as tr: commitopts = opts.copy() allctx = [repo[r] for r in revs] targetphase = max(c.phase() for c in allctx) if commitopts.get('message') or commitopts.get('logfile'): commitopts['edit'] = False else: msgs = ["HG: This is a fold of %d changesets." % len(allctx)] msgs += ["HG: Commit message of changeset %s.\n\n%s\n" % (c.rev(), c.description()) for c in allctx] commitopts['message'] = "\n".join(msgs) commitopts['edit'] = True newid, unusedvariable = common.rewrite(repo, root, allctx, head, [root.p1().node(), root.p2().node()], commitopts=commitopts) phases.retractboundary(repo, tr, targetphase, [newid]) replacements = {ctx.node(): (newid,) for ctx in allctx} nodechanges = {fm.hexfunc(ctx.node()): [fm.hexfunc(newid)] for ctx in allctx} fm.data(nodechanges=fm.formatdict(nodechanges)) scmutil.cleanupnodes(repo, replacements, 'fold') fm.condwrite(not ui.quiet, 'count', '%i changesets folded\n', len(revs)) if repo['.'].rev() in revs: hg.update(repo, newid) if torebase: folded = repo.revs('allsuccessors(%ld)', revs).last() common.restackonce(ui, repo, folded)
def uncommit(ui, repo, *pats, **opts): """uncommit part or all of a local changeset This command undoes the effect of a local commit, returning the affected files to their uncommitted state. This means that files modified or deleted in the changeset will be left unchanged, and so will remain modified in the working directory. If no files are specified, the commit will be pruned, unless --keep is given. """ opts = pycompat.byteskwargs(opts) cmdutil.checknotesize(ui, opts) cmdutil.resolvecommitoptions(ui, opts) with repo.wlock(), repo.lock(): m, a, r, d = repo.status()[:4] isdirtypath = any(set(m + a + r + d) & set(pats)) allowdirtywcopy = opts[ b'allow_dirty_working_copy'] or repo.ui.configbool( b'experimental', b'uncommitondirtywdir') if not allowdirtywcopy and (not pats or isdirtypath): cmdutil.bailifchanged( repo, hint=_(b'requires --allow-dirty-working-copy to uncommit'), ) old = repo[b'.'] rewriteutil.precheck(repo, [old.rev()], b'uncommit') if len(old.parents()) > 1: raise error.Abort(_(b"cannot uncommit merge changeset")) match = scmutil.match(old, pats, opts) # Check all explicitly given files; abort if there's a problem. if match.files(): s = old.status(old.p1(), match, listclean=True) eligible = set(s.added) | set(s.modified) | set(s.removed) badfiles = set(match.files()) - eligible # Naming a parent directory of an eligible file is OK, even # if not everything tracked in that directory can be # uncommitted. if badfiles: badfiles -= {f for f in util.dirs(eligible)} for f in sorted(badfiles): if f in s.clean: hint = _( b"file was not changed in working directory parent") elif repo.wvfs.exists(f): hint = _(b"file was untracked in working directory parent") else: hint = _(b"file does not exist") raise error.Abort( _(b'cannot uncommit "%s"') % scmutil.getuipathfn(repo)(f), hint=hint, ) with repo.transaction(b'uncommit'): if not (opts[b'message'] or opts[b'logfile']): opts[b'message'] = old.description() message = cmdutil.logmessage(ui, opts) keepcommit = pats if not keepcommit: if opts.get(b'keep') is not None: keepcommit = opts.get(b'keep') else: keepcommit = ui.configbool(b'experimental', b'uncommit.keep') newid = _commitfiltered( repo, old, match, keepcommit, message=message, user=opts.get(b'user'), date=opts.get(b'date'), ) if newid is None: ui.status(_(b"nothing to uncommit\n")) return 1 mapping = {} if newid != old.p1().node(): # Move local changes on filtered changeset mapping[old.node()] = (newid, ) else: # Fully removed the old commit mapping[old.node()] = () with repo.dirstate.parentchange(): scmutil.movedirstate(repo, repo[newid], match) scmutil.cleanupnodes(repo, mapping, b'uncommit', fixphase=True)
def metaedit(ui, repo, *revs, **opts): """edit commit information Edits the commit information for the specified revisions. By default, edits commit information for the working directory parent. With --fold, also folds multiple revisions into one if necessary. In this case, the given revisions must form a linear unbroken chain. .. container:: verbose Some examples: - Edit the commit message for the working directory parent:: hg metaedit - Change the username for the working directory parent:: hg metaedit --user 'New User <*****@*****.**>' - Combine all draft revisions that are ancestors of foo but not of @ into one:: hg metaedit --fold 'draft() and only(foo,@)' See :hg:`help phases` for more about draft revisions, and :hg:`help revsets` for more about the `draft()` and `only()` keywords. """ revs = list(revs) revs.extend(opts['rev']) if not revs: if opts['fold']: raise error.Abort(_('revisions must be specified with --fold')) revs = ['.'] wlock = lock = None try: wlock = repo.wlock() lock = repo.lock() revs = scmutil.revrange(repo, revs) if opts['fold']: root, head = fold._foldcheck(repo, revs) else: if repo.revs("%ld and public()", revs): raise error.Abort(_('cannot edit commit information for public ' 'revisions')) root = head = repo[revs.first()] wctx = repo[None] p1 = wctx.p1() tr = repo.transaction('metaedit') newp1 = None try: commitopts = opts.copy() allctx = [repo[r] for r in revs] if commitopts.get('message') or commitopts.get('logfile'): commitopts['edit'] = False else: if opts['fold']: msgs = [_("HG: This is a fold of %d changesets.") % len(allctx)] msgs += [_("HG: Commit message of changeset %s.\n\n%s\n") % (c.rev(), c.description()) for c in allctx] else: msgs = [head.description()] commitopts['message'] = "\n".join(msgs) commitopts['edit'] = True if root == head: # fast path: use metarewrite replacemap = {} # we need topological order allctx = sorted(allctx, key=lambda c: c.rev()) # all descendats that can be safely rewritten newunstable = common.newunstable(repo, revs) newunstablectx = sorted([repo[r] for r in newunstable], key=lambda c: c.rev()) def _rewritesingle(c, _commitopts): if _commitopts.get('edit', False): _commitopts['message'] = \ "HG: Commit message of changeset %s\n%s" %\ (str(c), c.description()) bases = [ replacemap.get(c.p1().node(), c.p1().node()), replacemap.get(c.p2().node(), c.p2().node()), ] newid, created = common.metarewrite(repo, c, bases, commitopts=_commitopts) if created: replacemap[c.node()] = newid for c in allctx: _rewritesingle(c, commitopts) if _histediting(repo): ui.note(_('during histedit, the descendants of ' 'the edited commit weren\'t auto-rebased\n')) else: for c in newunstablectx: _rewritesingle(c, {'date': commitopts.get('date') or None}) if p1.node() in replacemap: repo.setparents(replacemap[p1.node()]) if len(replacemap) > 0: mapping = dict(map(lambda oldnew: (oldnew[0], [oldnew[1]]), replacemap.iteritems())) scmutil.cleanupnodes(repo, mapping, 'metaedit') # TODO: set poroper phase boundaries (affects secret # phase only) else: ui.status(_("nothing changed\n")) return 1 else: # slow path: create a new commit targetphase = max(c.phase() for c in allctx) # TODO: if the author and message are the same, don't create a # new hash. Right now we create a new hash because the date can # be different. newid, created = common.rewrite( repo, root, allctx, head, [root.p1().node(), root.p2().node()], commitopts=commitopts) if created: if p1.rev() in revs: newp1 = newid phases.retractboundary(repo, tr, targetphase, [newid]) mapping = dict( [(repo[rev].node(), [newid]) for rev in revs]) scmutil.cleanupnodes(repo, mapping, 'metaedit') else: ui.status(_("nothing changed\n")) return 1 tr.close() finally: tr.release() if opts['fold']: ui.status(_('%i changesets folded\n') % len(revs)) if newp1 is not None: hg.update(repo, newp1) finally: lockmod.release(lock, wlock)