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]...')) |