diff --git a/hgext/record.py b/hgext/record.py --- a/hgext/record.py +++ b/hgext/record.py @@ -10,164 +10,12 @@ from mercurial.i18n import _ from mercurial import cmdutil, commands, extensions, hg, patch from mercurial import util -import copy, cStringIO, errno, os, shutil, tempfile +import cStringIO, errno, os, shutil, tempfile cmdtable = {} command = cmdutil.command(cmdtable) testedwith = 'internal' -def filterpatch(ui, headers): - """Interactively filter patch chunks into applied-only chunks""" - - def prompt(skipfile, skipall, query, chunk): - """prompt query, and process base inputs - - - y/n for the rest of file - - y/n for the rest - - ? (help) - - q (quit) - - Return True/False and possibly updated skipfile and skipall. - """ - newpatches = None - if skipall is not None: - return skipall, skipfile, skipall, newpatches - if skipfile is not None: - return skipfile, skipfile, skipall, newpatches - while True: - resps = _('[Ynesfdaq?]' - '$$ &Yes, record this change' - '$$ &No, skip this change' - '$$ &Edit this change manually' - '$$ &Skip remaining changes to this file' - '$$ Record remaining changes to this &file' - '$$ &Done, skip remaining changes and files' - '$$ Record &all changes to all remaining files' - '$$ &Quit, recording no changes' - '$$ &? (display help)') - r = ui.promptchoice("%s %s" % (query, resps)) - ui.write("\n") - if r == 8: # ? - for c, t in ui.extractchoices(resps)[1]: - ui.write('%s - %s\n' % (c, t.lower())) - continue - elif r == 0: # yes - ret = True - elif r == 1: # no - ret = False - elif r == 2: # Edit patch - if chunk is None: - ui.write(_('cannot edit patch for whole file')) - ui.write("\n") - continue - if chunk.header.binary(): - ui.write(_('cannot edit patch for binary file')) - ui.write("\n") - continue - # Patch comment based on the Git one (based on comment at end of - # http://mercurial.selenic.com/wiki/RecordExtension) - phelp = '---' + _(""" -To remove '-' lines, make them ' ' lines (context). -To remove '+' lines, delete them. -Lines starting with # will be removed from the patch. - -If the patch applies cleanly, the edited hunk will immediately be -added to the record list. If it does not apply cleanly, a rejects -file will be generated: you can use that when you try again. If -all lines of the hunk are removed, then the edit is aborted and -the hunk is left unchanged. -""") - (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-", - suffix=".diff", text=True) - ncpatchfp = None - try: - # Write the initial patch - f = os.fdopen(patchfd, "w") - chunk.header.write(f) - chunk.write(f) - f.write('\n'.join(['# ' + i for i in phelp.splitlines()])) - f.close() - # Start the editor and wait for it to complete - editor = ui.geteditor() - ui.system("%s \"%s\"" % (editor, patchfn), - environ={'HGUSER': ui.username()}, - onerr=util.Abort, errprefix=_("edit failed")) - # Remove comment lines - patchfp = open(patchfn) - ncpatchfp = cStringIO.StringIO() - for line in patchfp: - if not line.startswith('#'): - ncpatchfp.write(line) - patchfp.close() - ncpatchfp.seek(0) - newpatches = patch.parsepatch(ncpatchfp) - finally: - os.unlink(patchfn) - del ncpatchfp - # Signal that the chunk shouldn't be applied as-is, but - # provide the new patch to be used instead. - ret = False - elif r == 3: # Skip - ret = skipfile = False - elif r == 4: # file (Record remaining) - ret = skipfile = True - elif r == 5: # done, skip remaining - ret = skipall = False - elif r == 6: # all - ret = skipall = True - elif r == 7: # quit - raise util.Abort(_('user quit')) - return ret, skipfile, skipall, newpatches - - seen = set() - applied = {} # 'filename' -> [] of chunks - skipfile, skipall = None, None - pos, total = 1, sum(len(h.hunks) for h in headers) - for h in headers: - pos += len(h.hunks) - skipfile = None - fixoffset = 0 - hdr = ''.join(h.header) - if hdr in seen: - continue - seen.add(hdr) - if skipall is None: - h.pretty(ui) - msg = (_('examine changes to %s?') % - _(' and ').join("'%s'" % f for f in h.files())) - r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None) - if not r: - continue - applied[h.filename()] = [h] - if h.allhunks(): - applied[h.filename()] += h.hunks - continue - for i, chunk in enumerate(h.hunks): - if skipfile is None and skipall is None: - chunk.pretty(ui) - if total == 1: - msg = _("record this change to '%s'?") % chunk.filename() - else: - idx = pos - len(h.hunks) + i - msg = _("record change %d/%d to '%s'?") % (idx, total, - chunk.filename()) - r, skipfile, skipall, newpatches = prompt(skipfile, - skipall, msg, chunk) - if r: - if fixoffset: - chunk = copy.copy(chunk) - chunk.toline += fixoffset - applied[chunk.filename()].append(chunk) - elif newpatches is not None: - for newpatch in newpatches: - for newhunk in newpatch.hunks: - if fixoffset: - newhunk.toline += fixoffset - applied[newhunk.filename()].append(newhunk) - else: - fixoffset += chunk.removed - chunk.added - return sum([h for h in applied.itervalues() - if h[0].special() or len(h) > 1], []) @command("record", # same options as commit + white space diff options @@ -290,7 +138,7 @@ def dorecord(ui, repo, commitfunc, cmdsu # 1. filter patch, so we have intending-to apply subset of it try: - chunks = filterpatch(ui, patch.parsepatch(fp)) + chunks = patch.filterpatch(ui, patch.parsepatch(fp)) except patch.PatchError, err: raise util.Abort(_('error parsing patch: %s') % err) diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -6,7 +6,7 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -import cStringIO, email, os, errno, re, posixpath +import cStringIO, email, os, errno, re, posixpath, copy import tempfile, zlib, shutil # On python2.4 you have to import these by name or they fail to # load. This was not a problem on Python 2.7. @@ -908,6 +908,158 @@ class recordhunk(object): def __repr__(self): return '' % (self.filename(), self.fromline) +def filterpatch(ui, headers): + """Interactively filter patch chunks into applied-only chunks""" + + def prompt(skipfile, skipall, query, chunk): + """prompt query, and process base inputs + + - y/n for the rest of file + - y/n for the rest + - ? (help) + - q (quit) + + Return True/False and possibly updated skipfile and skipall. + """ + newpatches = None + if skipall is not None: + return skipall, skipfile, skipall, newpatches + if skipfile is not None: + return skipfile, skipfile, skipall, newpatches + while True: + resps = _('[Ynesfdaq?]' + '$$ &Yes, record this change' + '$$ &No, skip this change' + '$$ &Edit this change manually' + '$$ &Skip remaining changes to this file' + '$$ Record remaining changes to this &file' + '$$ &Done, skip remaining changes and files' + '$$ Record &all changes to all remaining files' + '$$ &Quit, recording no changes' + '$$ &? (display help)') + r = ui.promptchoice("%s %s" % (query, resps)) + ui.write("\n") + if r == 8: # ? + for c, t in ui.extractchoices(resps)[1]: + ui.write('%s - %s\n' % (c, t.lower())) + continue + elif r == 0: # yes + ret = True + elif r == 1: # no + ret = False + elif r == 2: # Edit patch + if chunk is None: + ui.write(_('cannot edit patch for whole file')) + ui.write("\n") + continue + if chunk.header.binary(): + ui.write(_('cannot edit patch for binary file')) + ui.write("\n") + continue + # Patch comment based on the Git one (based on comment at end of + # http://mercurial.selenic.com/wiki/RecordExtension) + phelp = '---' + _(""" +To remove '-' lines, make them ' ' lines (context). +To remove '+' lines, delete them. +Lines starting with # will be removed from the patch. + +If the patch applies cleanly, the edited hunk will immediately be +added to the record list. If it does not apply cleanly, a rejects +file will be generated: you can use that when you try again. If +all lines of the hunk are removed, then the edit is aborted and +the hunk is left unchanged. +""") + (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-", + suffix=".diff", text=True) + ncpatchfp = None + try: + # Write the initial patch + f = os.fdopen(patchfd, "w") + chunk.header.write(f) + chunk.write(f) + f.write('\n'.join(['# ' + i for i in phelp.splitlines()])) + f.close() + # Start the editor and wait for it to complete + editor = ui.geteditor() + ui.system("%s \"%s\"" % (editor, patchfn), + environ={'HGUSER': ui.username()}, + onerr=util.Abort, errprefix=_("edit failed")) + # Remove comment lines + patchfp = open(patchfn) + ncpatchfp = cStringIO.StringIO() + for line in patchfp: + if not line.startswith('#'): + ncpatchfp.write(line) + patchfp.close() + ncpatchfp.seek(0) + newpatches = parsepatch(ncpatchfp) + finally: + os.unlink(patchfn) + del ncpatchfp + # Signal that the chunk shouldn't be applied as-is, but + # provide the new patch to be used instead. + ret = False + elif r == 3: # Skip + ret = skipfile = False + elif r == 4: # file (Record remaining) + ret = skipfile = True + elif r == 5: # done, skip remaining + ret = skipall = False + elif r == 6: # all + ret = skipall = True + elif r == 7: # quit + raise util.Abort(_('user quit')) + return ret, skipfile, skipall, newpatches + + seen = set() + applied = {} # 'filename' -> [] of chunks + skipfile, skipall = None, None + pos, total = 1, sum(len(h.hunks) for h in headers) + for h in headers: + pos += len(h.hunks) + skipfile = None + fixoffset = 0 + hdr = ''.join(h.header) + if hdr in seen: + continue + seen.add(hdr) + if skipall is None: + h.pretty(ui) + msg = (_('examine changes to %s?') % + _(' and ').join("'%s'" % f for f in h.files())) + r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None) + if not r: + continue + applied[h.filename()] = [h] + if h.allhunks(): + applied[h.filename()] += h.hunks + continue + for i, chunk in enumerate(h.hunks): + if skipfile is None and skipall is None: + chunk.pretty(ui) + if total == 1: + msg = _("record this change to '%s'?") % chunk.filename() + else: + idx = pos - len(h.hunks) + i + msg = _("record change %d/%d to '%s'?") % (idx, total, + chunk.filename()) + r, skipfile, skipall, newpatches = prompt(skipfile, + skipall, msg, chunk) + if r: + if fixoffset: + chunk = copy.copy(chunk) + chunk.toline += fixoffset + applied[chunk.filename()].append(chunk) + elif newpatches is not None: + for newpatch in newpatches: + for newhunk in newpatch.hunks: + if fixoffset: + newhunk.toline += fixoffset + applied[newhunk.filename()].append(newhunk) + else: + fixoffset += chunk.removed - chunk.added + return sum([h for h in applied.itervalues() + if h[0].special() or len(h) > 1], []) class hunk(object): def __init__(self, desc, num, lr, context): self.number = num