comparison mercurial/patch.py @ 24269:9a745ced79a9

record: move filterpatch from record to patch Part of a series of patches to move record from hgext to core
author Laurent Charignon <lcharignon@fb.com>
date Tue, 10 Mar 2015 14:42:07 -0700
parents cf7d252d8c30
children 6ddc86eedc3b
comparison
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]