Show More
@@ -10,164 +10,12 | |||||
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 |
|
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 | |
@@ -290,7 +138,7 def dorecord(ui, repo, commitfunc, cmdsu | |||||
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 |
@@ -6,7 +6,7 | |||||
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. | |
@@ -908,6 +908,158 class recordhunk(object): | |||||
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 |
General Comments 0
You need to be logged in to leave comments.
Login now