hgext/record.py
changeset 24269 9a745ced79a9
parent 24268 cf7d252d8c30
child 24270 c256ae48fd26
equal deleted inserted replaced
24268:cf7d252d8c30 24269:9a745ced79a9
     8 '''commands to interactively select changes for commit/qrefresh'''
     8 '''commands to interactively select changes for commit/qrefresh'''
     9 
     9 
    10 from mercurial.i18n import _
    10 from mercurial.i18n import _
    11 from mercurial import cmdutil, commands, extensions, hg, patch
    11 from mercurial import cmdutil, commands, extensions, hg, patch
    12 from mercurial import util
    12 from mercurial import util
    13 import copy, cStringIO, errno, os, shutil, tempfile
    13 import cStringIO, errno, os, shutil, tempfile
    14 
    14 
    15 cmdtable = {}
    15 cmdtable = {}
    16 command = cmdutil.command(cmdtable)
    16 command = cmdutil.command(cmdtable)
    17 testedwith = 'internal'
    17 testedwith = 'internal'
    18 
    18 
    19 def filterpatch(ui, headers):
       
    20     """Interactively filter patch chunks into applied-only chunks"""
       
    21 
       
    22     def prompt(skipfile, skipall, query, chunk):
       
    23         """prompt query, and process base inputs
       
    24 
       
    25         - y/n for the rest of file
       
    26         - y/n for the rest
       
    27         - ? (help)
       
    28         - q (quit)
       
    29 
       
    30         Return True/False and possibly updated skipfile and skipall.
       
    31         """
       
    32         newpatches = None
       
    33         if skipall is not None:
       
    34             return skipall, skipfile, skipall, newpatches
       
    35         if skipfile is not None:
       
    36             return skipfile, skipfile, skipall, newpatches
       
    37         while True:
       
    38             resps = _('[Ynesfdaq?]'
       
    39                       '$$ &Yes, record this change'
       
    40                       '$$ &No, skip this change'
       
    41                       '$$ &Edit this change manually'
       
    42                       '$$ &Skip remaining changes to this file'
       
    43                       '$$ Record remaining changes to this &file'
       
    44                       '$$ &Done, skip remaining changes and files'
       
    45                       '$$ Record &all changes to all remaining files'
       
    46                       '$$ &Quit, recording no changes'
       
    47                       '$$ &? (display help)')
       
    48             r = ui.promptchoice("%s %s" % (query, resps))
       
    49             ui.write("\n")
       
    50             if r == 8: # ?
       
    51                 for c, t in ui.extractchoices(resps)[1]:
       
    52                     ui.write('%s - %s\n' % (c, t.lower()))
       
    53                 continue
       
    54             elif r == 0: # yes
       
    55                 ret = True
       
    56             elif r == 1: # no
       
    57                 ret = False
       
    58             elif r == 2: # Edit patch
       
    59                 if chunk is None:
       
    60                     ui.write(_('cannot edit patch for whole file'))
       
    61                     ui.write("\n")
       
    62                     continue
       
    63                 if chunk.header.binary():
       
    64                     ui.write(_('cannot edit patch for binary file'))
       
    65                     ui.write("\n")
       
    66                     continue
       
    67                 # Patch comment based on the Git one (based on comment at end of
       
    68                 # http://mercurial.selenic.com/wiki/RecordExtension)
       
    69                 phelp = '---' + _("""
       
    70 To remove '-' lines, make them ' ' lines (context).
       
    71 To remove '+' lines, delete them.
       
    72 Lines starting with # will be removed from the patch.
       
    73 
       
    74 If the patch applies cleanly, the edited hunk will immediately be
       
    75 added to the record list. If it does not apply cleanly, a rejects
       
    76 file will be generated: you can use that when you try again. If
       
    77 all lines of the hunk are removed, then the edit is aborted and
       
    78 the hunk is left unchanged.
       
    79 """)
       
    80                 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
       
    81                         suffix=".diff", text=True)
       
    82                 ncpatchfp = None
       
    83                 try:
       
    84                     # Write the initial patch
       
    85                     f = os.fdopen(patchfd, "w")
       
    86                     chunk.header.write(f)
       
    87                     chunk.write(f)
       
    88                     f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
       
    89                     f.close()
       
    90                     # Start the editor and wait for it to complete
       
    91                     editor = ui.geteditor()
       
    92                     ui.system("%s \"%s\"" % (editor, patchfn),
       
    93                               environ={'HGUSER': ui.username()},
       
    94                               onerr=util.Abort, errprefix=_("edit failed"))
       
    95                     # Remove comment lines
       
    96                     patchfp = open(patchfn)
       
    97                     ncpatchfp = cStringIO.StringIO()
       
    98                     for line in patchfp:
       
    99                         if not line.startswith('#'):
       
   100                             ncpatchfp.write(line)
       
   101                     patchfp.close()
       
   102                     ncpatchfp.seek(0)
       
   103                     newpatches = patch.parsepatch(ncpatchfp)
       
   104                 finally:
       
   105                     os.unlink(patchfn)
       
   106                     del ncpatchfp
       
   107                 # Signal that the chunk shouldn't be applied as-is, but
       
   108                 # provide the new patch to be used instead.
       
   109                 ret = False
       
   110             elif r == 3: # Skip
       
   111                 ret = skipfile = False
       
   112             elif r == 4: # file (Record remaining)
       
   113                 ret = skipfile = True
       
   114             elif r == 5: # done, skip remaining
       
   115                 ret = skipall = False
       
   116             elif r == 6: # all
       
   117                 ret = skipall = True
       
   118             elif r == 7: # quit
       
   119                 raise util.Abort(_('user quit'))
       
   120             return ret, skipfile, skipall, newpatches
       
   121 
       
   122     seen = set()
       
   123     applied = {}        # 'filename' -> [] of chunks
       
   124     skipfile, skipall = None, None
       
   125     pos, total = 1, sum(len(h.hunks) for h in headers)
       
   126     for h in headers:
       
   127         pos += len(h.hunks)
       
   128         skipfile = None
       
   129         fixoffset = 0
       
   130         hdr = ''.join(h.header)
       
   131         if hdr in seen:
       
   132             continue
       
   133         seen.add(hdr)
       
   134         if skipall is None:
       
   135             h.pretty(ui)
       
   136         msg = (_('examine changes to %s?') %
       
   137                _(' and ').join("'%s'" % f for f in h.files()))
       
   138         r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
       
   139         if not r:
       
   140             continue
       
   141         applied[h.filename()] = [h]
       
   142         if h.allhunks():
       
   143             applied[h.filename()] += h.hunks
       
   144             continue
       
   145         for i, chunk in enumerate(h.hunks):
       
   146             if skipfile is None and skipall is None:
       
   147                 chunk.pretty(ui)
       
   148             if total == 1:
       
   149                 msg = _("record this change to '%s'?") % chunk.filename()
       
   150             else:
       
   151                 idx = pos - len(h.hunks) + i
       
   152                 msg = _("record change %d/%d to '%s'?") % (idx, total,
       
   153                                                            chunk.filename())
       
   154             r, skipfile, skipall, newpatches = prompt(skipfile,
       
   155                     skipall, msg, chunk)
       
   156             if r:
       
   157                 if fixoffset:
       
   158                     chunk = copy.copy(chunk)
       
   159                     chunk.toline += fixoffset
       
   160                 applied[chunk.filename()].append(chunk)
       
   161             elif newpatches is not None:
       
   162                 for newpatch in newpatches:
       
   163                     for newhunk in newpatch.hunks:
       
   164                         if fixoffset:
       
   165                             newhunk.toline += fixoffset
       
   166                         applied[newhunk.filename()].append(newhunk)
       
   167             else:
       
   168                 fixoffset += chunk.removed - chunk.added
       
   169     return sum([h for h in applied.itervalues()
       
   170                if h[0].special() or len(h) > 1], [])
       
   171 
    19 
   172 @command("record",
    20 @command("record",
   173          # same options as commit + white space diff options
    21          # same options as commit + white space diff options
   174          commands.table['^commit|ci'][1][:] + commands.diffwsopts,
    22          commands.table['^commit|ci'][1][:] + commands.diffwsopts,
   175           _('hg record [OPTION]... [FILE]...'))
    23           _('hg record [OPTION]... [FILE]...'))
   288         fp.write(''.join(originalchunks))
   136         fp.write(''.join(originalchunks))
   289         fp.seek(0)
   137         fp.seek(0)
   290 
   138 
   291         # 1. filter patch, so we have intending-to apply subset of it
   139         # 1. filter patch, so we have intending-to apply subset of it
   292         try:
   140         try:
   293             chunks = filterpatch(ui, patch.parsepatch(fp))
   141             chunks = patch.filterpatch(ui, patch.parsepatch(fp))
   294         except patch.PatchError, err:
   142         except patch.PatchError, err:
   295             raise util.Abort(_('error parsing patch: %s') % err)
   143             raise util.Abort(_('error parsing patch: %s') % err)
   296 
   144 
   297         del fp
   145         del fp
   298 
   146