mercurial/patch.py
changeset 24269 9a745ced79a9
parent 24268 cf7d252d8c30
child 24306 6ddc86eedc3b
equal deleted inserted replaced
24268:cf7d252d8c30 24269:9a745ced79a9
     4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
     4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
     5 #
     5 #
     6 # This software may be used and distributed according to the terms of the
     6 # This software may be used and distributed according to the terms of the
     7 # GNU General Public License version 2 or any later version.
     7 # GNU General Public License version 2 or any later version.
     8 
     8 
     9 import cStringIO, email, os, errno, re, posixpath
     9 import cStringIO, email, os, errno, re, posixpath, copy
    10 import tempfile, zlib, shutil
    10 import tempfile, zlib, shutil
    11 # On python2.4 you have to import these by name or they fail to
    11 # On python2.4 you have to import these by name or they fail to
    12 # load. This was not a problem on Python 2.7.
    12 # load. This was not a problem on Python 2.7.
    13 import email.Generator
    13 import email.Generator
    14 import email.Parser
    14 import email.Parser
   906         return self.header.filename()
   906         return self.header.filename()
   907 
   907 
   908     def __repr__(self):
   908     def __repr__(self):
   909         return '<hunk %r@%d>' % (self.filename(), self.fromline)
   909         return '<hunk %r@%d>' % (self.filename(), self.fromline)
   910 
   910 
       
   911 def filterpatch(ui, headers):
       
   912     """Interactively filter patch chunks into applied-only chunks"""
       
   913 
       
   914     def prompt(skipfile, skipall, query, chunk):
       
   915         """prompt query, and process base inputs
       
   916 
       
   917         - y/n for the rest of file
       
   918         - y/n for the rest
       
   919         - ? (help)
       
   920         - q (quit)
       
   921 
       
   922         Return True/False and possibly updated skipfile and skipall.
       
   923         """
       
   924         newpatches = None
       
   925         if skipall is not None:
       
   926             return skipall, skipfile, skipall, newpatches
       
   927         if skipfile is not None:
       
   928             return skipfile, skipfile, skipall, newpatches
       
   929         while True:
       
   930             resps = _('[Ynesfdaq?]'
       
   931                       '$$ &Yes, record this change'
       
   932                       '$$ &No, skip this change'
       
   933                       '$$ &Edit this change manually'
       
   934                       '$$ &Skip remaining changes to this file'
       
   935                       '$$ Record remaining changes to this &file'
       
   936                       '$$ &Done, skip remaining changes and files'
       
   937                       '$$ Record &all changes to all remaining files'
       
   938                       '$$ &Quit, recording no changes'
       
   939                       '$$ &? (display help)')
       
   940             r = ui.promptchoice("%s %s" % (query, resps))
       
   941             ui.write("\n")
       
   942             if r == 8: # ?
       
   943                 for c, t in ui.extractchoices(resps)[1]:
       
   944                     ui.write('%s - %s\n' % (c, t.lower()))
       
   945                 continue
       
   946             elif r == 0: # yes
       
   947                 ret = True
       
   948             elif r == 1: # no
       
   949                 ret = False
       
   950             elif r == 2: # Edit patch
       
   951                 if chunk is None:
       
   952                     ui.write(_('cannot edit patch for whole file'))
       
   953                     ui.write("\n")
       
   954                     continue
       
   955                 if chunk.header.binary():
       
   956                     ui.write(_('cannot edit patch for binary file'))
       
   957                     ui.write("\n")
       
   958                     continue
       
   959                 # Patch comment based on the Git one (based on comment at end of
       
   960                 # http://mercurial.selenic.com/wiki/RecordExtension)
       
   961                 phelp = '---' + _("""
       
   962 To remove '-' lines, make them ' ' lines (context).
       
   963 To remove '+' lines, delete them.
       
   964 Lines starting with # will be removed from the patch.
       
   965 
       
   966 If the patch applies cleanly, the edited hunk will immediately be
       
   967 added to the record list. If it does not apply cleanly, a rejects
       
   968 file will be generated: you can use that when you try again. If
       
   969 all lines of the hunk are removed, then the edit is aborted and
       
   970 the hunk is left unchanged.
       
   971 """)
       
   972                 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
       
   973                         suffix=".diff", text=True)
       
   974                 ncpatchfp = None
       
   975                 try:
       
   976                     # Write the initial patch
       
   977                     f = os.fdopen(patchfd, "w")
       
   978                     chunk.header.write(f)
       
   979                     chunk.write(f)
       
   980                     f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
       
   981                     f.close()
       
   982                     # Start the editor and wait for it to complete
       
   983                     editor = ui.geteditor()
       
   984                     ui.system("%s \"%s\"" % (editor, patchfn),
       
   985                               environ={'HGUSER': ui.username()},
       
   986                               onerr=util.Abort, errprefix=_("edit failed"))
       
   987                     # Remove comment lines
       
   988                     patchfp = open(patchfn)
       
   989                     ncpatchfp = cStringIO.StringIO()
       
   990                     for line in patchfp:
       
   991                         if not line.startswith('#'):
       
   992                             ncpatchfp.write(line)
       
   993                     patchfp.close()
       
   994                     ncpatchfp.seek(0)
       
   995                     newpatches = parsepatch(ncpatchfp)
       
   996                 finally:
       
   997                     os.unlink(patchfn)
       
   998                     del ncpatchfp
       
   999                 # Signal that the chunk shouldn't be applied as-is, but
       
  1000                 # provide the new patch to be used instead.
       
  1001                 ret = False
       
  1002             elif r == 3: # Skip
       
  1003                 ret = skipfile = False
       
  1004             elif r == 4: # file (Record remaining)
       
  1005                 ret = skipfile = True
       
  1006             elif r == 5: # done, skip remaining
       
  1007                 ret = skipall = False
       
  1008             elif r == 6: # all
       
  1009                 ret = skipall = True
       
  1010             elif r == 7: # quit
       
  1011                 raise util.Abort(_('user quit'))
       
  1012             return ret, skipfile, skipall, newpatches
       
  1013 
       
  1014     seen = set()
       
  1015     applied = {}        # 'filename' -> [] of chunks
       
  1016     skipfile, skipall = None, None
       
  1017     pos, total = 1, sum(len(h.hunks) for h in headers)
       
  1018     for h in headers:
       
  1019         pos += len(h.hunks)
       
  1020         skipfile = None
       
  1021         fixoffset = 0
       
  1022         hdr = ''.join(h.header)
       
  1023         if hdr in seen:
       
  1024             continue
       
  1025         seen.add(hdr)
       
  1026         if skipall is None:
       
  1027             h.pretty(ui)
       
  1028         msg = (_('examine changes to %s?') %
       
  1029                _(' and ').join("'%s'" % f for f in h.files()))
       
  1030         r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
       
  1031         if not r:
       
  1032             continue
       
  1033         applied[h.filename()] = [h]
       
  1034         if h.allhunks():
       
  1035             applied[h.filename()] += h.hunks
       
  1036             continue
       
  1037         for i, chunk in enumerate(h.hunks):
       
  1038             if skipfile is None and skipall is None:
       
  1039                 chunk.pretty(ui)
       
  1040             if total == 1:
       
  1041                 msg = _("record this change to '%s'?") % chunk.filename()
       
  1042             else:
       
  1043                 idx = pos - len(h.hunks) + i
       
  1044                 msg = _("record change %d/%d to '%s'?") % (idx, total,
       
  1045                                                            chunk.filename())
       
  1046             r, skipfile, skipall, newpatches = prompt(skipfile,
       
  1047                     skipall, msg, chunk)
       
  1048             if r:
       
  1049                 if fixoffset:
       
  1050                     chunk = copy.copy(chunk)
       
  1051                     chunk.toline += fixoffset
       
  1052                 applied[chunk.filename()].append(chunk)
       
  1053             elif newpatches is not None:
       
  1054                 for newpatch in newpatches:
       
  1055                     for newhunk in newpatch.hunks:
       
  1056                         if fixoffset:
       
  1057                             newhunk.toline += fixoffset
       
  1058                         applied[newhunk.filename()].append(newhunk)
       
  1059             else:
       
  1060                 fixoffset += chunk.removed - chunk.added
       
  1061     return sum([h for h in applied.itervalues()
       
  1062                if h[0].special() or len(h) > 1], [])
   911 class hunk(object):
  1063 class hunk(object):
   912     def __init__(self, desc, num, lr, context):
  1064     def __init__(self, desc, num, lr, context):
   913         self.number = num
  1065         self.number = num
   914         self.desc = desc
  1066         self.desc = desc
   915         self.hunk = [desc]
  1067         self.hunk = [desc]