##// END OF EJS Templates
record: add checkunfinished support (issue3955)
Matt Mackall -
r19497:e012a200 stable
parent child Browse files
Show More
@@ -1,680 +1,681 b''
1 # record.py
1 # record.py
2 #
2 #
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
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 gettext, _
10 from mercurial.i18n import gettext, _
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, re, shutil, tempfile
13 import copy, cStringIO, errno, os, re, 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 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
19 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
20
20
21 diffopts = [
21 diffopts = [
22 ('w', 'ignore-all-space', False,
22 ('w', 'ignore-all-space', False,
23 _('ignore white space when comparing lines')),
23 _('ignore white space when comparing lines')),
24 ('b', 'ignore-space-change', None,
24 ('b', 'ignore-space-change', None,
25 _('ignore changes in the amount of white space')),
25 _('ignore changes in the amount of white space')),
26 ('B', 'ignore-blank-lines', None,
26 ('B', 'ignore-blank-lines', None,
27 _('ignore changes whose lines are all blank')),
27 _('ignore changes whose lines are all blank')),
28 ]
28 ]
29
29
30 def scanpatch(fp):
30 def scanpatch(fp):
31 """like patch.iterhunks, but yield different events
31 """like patch.iterhunks, but yield different events
32
32
33 - ('file', [header_lines + fromfile + tofile])
33 - ('file', [header_lines + fromfile + tofile])
34 - ('context', [context_lines])
34 - ('context', [context_lines])
35 - ('hunk', [hunk_lines])
35 - ('hunk', [hunk_lines])
36 - ('range', (-start,len, +start,len, proc))
36 - ('range', (-start,len, +start,len, proc))
37 """
37 """
38 lr = patch.linereader(fp)
38 lr = patch.linereader(fp)
39
39
40 def scanwhile(first, p):
40 def scanwhile(first, p):
41 """scan lr while predicate holds"""
41 """scan lr while predicate holds"""
42 lines = [first]
42 lines = [first]
43 while True:
43 while True:
44 line = lr.readline()
44 line = lr.readline()
45 if not line:
45 if not line:
46 break
46 break
47 if p(line):
47 if p(line):
48 lines.append(line)
48 lines.append(line)
49 else:
49 else:
50 lr.push(line)
50 lr.push(line)
51 break
51 break
52 return lines
52 return lines
53
53
54 while True:
54 while True:
55 line = lr.readline()
55 line = lr.readline()
56 if not line:
56 if not line:
57 break
57 break
58 if line.startswith('diff --git a/') or line.startswith('diff -r '):
58 if line.startswith('diff --git a/') or line.startswith('diff -r '):
59 def notheader(line):
59 def notheader(line):
60 s = line.split(None, 1)
60 s = line.split(None, 1)
61 return not s or s[0] not in ('---', 'diff')
61 return not s or s[0] not in ('---', 'diff')
62 header = scanwhile(line, notheader)
62 header = scanwhile(line, notheader)
63 fromfile = lr.readline()
63 fromfile = lr.readline()
64 if fromfile.startswith('---'):
64 if fromfile.startswith('---'):
65 tofile = lr.readline()
65 tofile = lr.readline()
66 header += [fromfile, tofile]
66 header += [fromfile, tofile]
67 else:
67 else:
68 lr.push(fromfile)
68 lr.push(fromfile)
69 yield 'file', header
69 yield 'file', header
70 elif line[0] == ' ':
70 elif line[0] == ' ':
71 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
71 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
72 elif line[0] in '-+':
72 elif line[0] in '-+':
73 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
73 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
74 else:
74 else:
75 m = lines_re.match(line)
75 m = lines_re.match(line)
76 if m:
76 if m:
77 yield 'range', m.groups()
77 yield 'range', m.groups()
78 else:
78 else:
79 yield 'other', line
79 yield 'other', line
80
80
81 class header(object):
81 class header(object):
82 """patch header
82 """patch header
83
83
84 XXX shouldn't we move this to mercurial/patch.py ?
84 XXX shouldn't we move this to mercurial/patch.py ?
85 """
85 """
86 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
86 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
87 diff_re = re.compile('diff -r .* (.*)$')
87 diff_re = re.compile('diff -r .* (.*)$')
88 allhunks_re = re.compile('(?:index|new file|deleted file) ')
88 allhunks_re = re.compile('(?:index|new file|deleted file) ')
89 pretty_re = re.compile('(?:new file|deleted file) ')
89 pretty_re = re.compile('(?:new file|deleted file) ')
90 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
90 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
91
91
92 def __init__(self, header):
92 def __init__(self, header):
93 self.header = header
93 self.header = header
94 self.hunks = []
94 self.hunks = []
95
95
96 def binary(self):
96 def binary(self):
97 return util.any(h.startswith('index ') for h in self.header)
97 return util.any(h.startswith('index ') for h in self.header)
98
98
99 def pretty(self, fp):
99 def pretty(self, fp):
100 for h in self.header:
100 for h in self.header:
101 if h.startswith('index '):
101 if h.startswith('index '):
102 fp.write(_('this modifies a binary file (all or nothing)\n'))
102 fp.write(_('this modifies a binary file (all or nothing)\n'))
103 break
103 break
104 if self.pretty_re.match(h):
104 if self.pretty_re.match(h):
105 fp.write(h)
105 fp.write(h)
106 if self.binary():
106 if self.binary():
107 fp.write(_('this is a binary file\n'))
107 fp.write(_('this is a binary file\n'))
108 break
108 break
109 if h.startswith('---'):
109 if h.startswith('---'):
110 fp.write(_('%d hunks, %d lines changed\n') %
110 fp.write(_('%d hunks, %d lines changed\n') %
111 (len(self.hunks),
111 (len(self.hunks),
112 sum([max(h.added, h.removed) for h in self.hunks])))
112 sum([max(h.added, h.removed) for h in self.hunks])))
113 break
113 break
114 fp.write(h)
114 fp.write(h)
115
115
116 def write(self, fp):
116 def write(self, fp):
117 fp.write(''.join(self.header))
117 fp.write(''.join(self.header))
118
118
119 def allhunks(self):
119 def allhunks(self):
120 return util.any(self.allhunks_re.match(h) for h in self.header)
120 return util.any(self.allhunks_re.match(h) for h in self.header)
121
121
122 def files(self):
122 def files(self):
123 match = self.diffgit_re.match(self.header[0])
123 match = self.diffgit_re.match(self.header[0])
124 if match:
124 if match:
125 fromfile, tofile = match.groups()
125 fromfile, tofile = match.groups()
126 if fromfile == tofile:
126 if fromfile == tofile:
127 return [fromfile]
127 return [fromfile]
128 return [fromfile, tofile]
128 return [fromfile, tofile]
129 else:
129 else:
130 return self.diff_re.match(self.header[0]).groups()
130 return self.diff_re.match(self.header[0]).groups()
131
131
132 def filename(self):
132 def filename(self):
133 return self.files()[-1]
133 return self.files()[-1]
134
134
135 def __repr__(self):
135 def __repr__(self):
136 return '<header %s>' % (' '.join(map(repr, self.files())))
136 return '<header %s>' % (' '.join(map(repr, self.files())))
137
137
138 def special(self):
138 def special(self):
139 return util.any(self.special_re.match(h) for h in self.header)
139 return util.any(self.special_re.match(h) for h in self.header)
140
140
141 def countchanges(hunk):
141 def countchanges(hunk):
142 """hunk -> (n+,n-)"""
142 """hunk -> (n+,n-)"""
143 add = len([h for h in hunk if h[0] == '+'])
143 add = len([h for h in hunk if h[0] == '+'])
144 rem = len([h for h in hunk if h[0] == '-'])
144 rem = len([h for h in hunk if h[0] == '-'])
145 return add, rem
145 return add, rem
146
146
147 class hunk(object):
147 class hunk(object):
148 """patch hunk
148 """patch hunk
149
149
150 XXX shouldn't we merge this with patch.hunk ?
150 XXX shouldn't we merge this with patch.hunk ?
151 """
151 """
152 maxcontext = 3
152 maxcontext = 3
153
153
154 def __init__(self, header, fromline, toline, proc, before, hunk, after):
154 def __init__(self, header, fromline, toline, proc, before, hunk, after):
155 def trimcontext(number, lines):
155 def trimcontext(number, lines):
156 delta = len(lines) - self.maxcontext
156 delta = len(lines) - self.maxcontext
157 if False and delta > 0:
157 if False and delta > 0:
158 return number + delta, lines[:self.maxcontext]
158 return number + delta, lines[:self.maxcontext]
159 return number, lines
159 return number, lines
160
160
161 self.header = header
161 self.header = header
162 self.fromline, self.before = trimcontext(fromline, before)
162 self.fromline, self.before = trimcontext(fromline, before)
163 self.toline, self.after = trimcontext(toline, after)
163 self.toline, self.after = trimcontext(toline, after)
164 self.proc = proc
164 self.proc = proc
165 self.hunk = hunk
165 self.hunk = hunk
166 self.added, self.removed = countchanges(self.hunk)
166 self.added, self.removed = countchanges(self.hunk)
167
167
168 def write(self, fp):
168 def write(self, fp):
169 delta = len(self.before) + len(self.after)
169 delta = len(self.before) + len(self.after)
170 if self.after and self.after[-1] == '\\ No newline at end of file\n':
170 if self.after and self.after[-1] == '\\ No newline at end of file\n':
171 delta -= 1
171 delta -= 1
172 fromlen = delta + self.removed
172 fromlen = delta + self.removed
173 tolen = delta + self.added
173 tolen = delta + self.added
174 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
174 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
175 (self.fromline, fromlen, self.toline, tolen,
175 (self.fromline, fromlen, self.toline, tolen,
176 self.proc and (' ' + self.proc)))
176 self.proc and (' ' + self.proc)))
177 fp.write(''.join(self.before + self.hunk + self.after))
177 fp.write(''.join(self.before + self.hunk + self.after))
178
178
179 pretty = write
179 pretty = write
180
180
181 def filename(self):
181 def filename(self):
182 return self.header.filename()
182 return self.header.filename()
183
183
184 def __repr__(self):
184 def __repr__(self):
185 return '<hunk %r@%d>' % (self.filename(), self.fromline)
185 return '<hunk %r@%d>' % (self.filename(), self.fromline)
186
186
187 def parsepatch(fp):
187 def parsepatch(fp):
188 """patch -> [] of headers -> [] of hunks """
188 """patch -> [] of headers -> [] of hunks """
189 class parser(object):
189 class parser(object):
190 """patch parsing state machine"""
190 """patch parsing state machine"""
191 def __init__(self):
191 def __init__(self):
192 self.fromline = 0
192 self.fromline = 0
193 self.toline = 0
193 self.toline = 0
194 self.proc = ''
194 self.proc = ''
195 self.header = None
195 self.header = None
196 self.context = []
196 self.context = []
197 self.before = []
197 self.before = []
198 self.hunk = []
198 self.hunk = []
199 self.headers = []
199 self.headers = []
200
200
201 def addrange(self, limits):
201 def addrange(self, limits):
202 fromstart, fromend, tostart, toend, proc = limits
202 fromstart, fromend, tostart, toend, proc = limits
203 self.fromline = int(fromstart)
203 self.fromline = int(fromstart)
204 self.toline = int(tostart)
204 self.toline = int(tostart)
205 self.proc = proc
205 self.proc = proc
206
206
207 def addcontext(self, context):
207 def addcontext(self, context):
208 if self.hunk:
208 if self.hunk:
209 h = hunk(self.header, self.fromline, self.toline, self.proc,
209 h = hunk(self.header, self.fromline, self.toline, self.proc,
210 self.before, self.hunk, context)
210 self.before, self.hunk, context)
211 self.header.hunks.append(h)
211 self.header.hunks.append(h)
212 self.fromline += len(self.before) + h.removed
212 self.fromline += len(self.before) + h.removed
213 self.toline += len(self.before) + h.added
213 self.toline += len(self.before) + h.added
214 self.before = []
214 self.before = []
215 self.hunk = []
215 self.hunk = []
216 self.proc = ''
216 self.proc = ''
217 self.context = context
217 self.context = context
218
218
219 def addhunk(self, hunk):
219 def addhunk(self, hunk):
220 if self.context:
220 if self.context:
221 self.before = self.context
221 self.before = self.context
222 self.context = []
222 self.context = []
223 self.hunk = hunk
223 self.hunk = hunk
224
224
225 def newfile(self, hdr):
225 def newfile(self, hdr):
226 self.addcontext([])
226 self.addcontext([])
227 h = header(hdr)
227 h = header(hdr)
228 self.headers.append(h)
228 self.headers.append(h)
229 self.header = h
229 self.header = h
230
230
231 def addother(self, line):
231 def addother(self, line):
232 pass # 'other' lines are ignored
232 pass # 'other' lines are ignored
233
233
234 def finished(self):
234 def finished(self):
235 self.addcontext([])
235 self.addcontext([])
236 return self.headers
236 return self.headers
237
237
238 transitions = {
238 transitions = {
239 'file': {'context': addcontext,
239 'file': {'context': addcontext,
240 'file': newfile,
240 'file': newfile,
241 'hunk': addhunk,
241 'hunk': addhunk,
242 'range': addrange},
242 'range': addrange},
243 'context': {'file': newfile,
243 'context': {'file': newfile,
244 'hunk': addhunk,
244 'hunk': addhunk,
245 'range': addrange,
245 'range': addrange,
246 'other': addother},
246 'other': addother},
247 'hunk': {'context': addcontext,
247 'hunk': {'context': addcontext,
248 'file': newfile,
248 'file': newfile,
249 'range': addrange},
249 'range': addrange},
250 'range': {'context': addcontext,
250 'range': {'context': addcontext,
251 'hunk': addhunk},
251 'hunk': addhunk},
252 'other': {'other': addother},
252 'other': {'other': addother},
253 }
253 }
254
254
255 p = parser()
255 p = parser()
256
256
257 state = 'context'
257 state = 'context'
258 for newstate, data in scanpatch(fp):
258 for newstate, data in scanpatch(fp):
259 try:
259 try:
260 p.transitions[state][newstate](p, data)
260 p.transitions[state][newstate](p, data)
261 except KeyError:
261 except KeyError:
262 raise patch.PatchError('unhandled transition: %s -> %s' %
262 raise patch.PatchError('unhandled transition: %s -> %s' %
263 (state, newstate))
263 (state, newstate))
264 state = newstate
264 state = newstate
265 return p.finished()
265 return p.finished()
266
266
267 def filterpatch(ui, headers):
267 def filterpatch(ui, headers):
268 """Interactively filter patch chunks into applied-only chunks"""
268 """Interactively filter patch chunks into applied-only chunks"""
269
269
270 def prompt(skipfile, skipall, query, chunk):
270 def prompt(skipfile, skipall, query, chunk):
271 """prompt query, and process base inputs
271 """prompt query, and process base inputs
272
272
273 - y/n for the rest of file
273 - y/n for the rest of file
274 - y/n for the rest
274 - y/n for the rest
275 - ? (help)
275 - ? (help)
276 - q (quit)
276 - q (quit)
277
277
278 Return True/False and possibly updated skipfile and skipall.
278 Return True/False and possibly updated skipfile and skipall.
279 """
279 """
280 newpatches = None
280 newpatches = None
281 if skipall is not None:
281 if skipall is not None:
282 return skipall, skipfile, skipall, newpatches
282 return skipall, skipfile, skipall, newpatches
283 if skipfile is not None:
283 if skipfile is not None:
284 return skipfile, skipfile, skipall, newpatches
284 return skipfile, skipfile, skipall, newpatches
285 while True:
285 while True:
286 resps = _('[Ynesfdaq?]'
286 resps = _('[Ynesfdaq?]'
287 '$$ &Yes, record this change'
287 '$$ &Yes, record this change'
288 '$$ &No, skip this change'
288 '$$ &No, skip this change'
289 '$$ &Edit the change manually'
289 '$$ &Edit the change manually'
290 '$$ &Skip remaining changes to this file'
290 '$$ &Skip remaining changes to this file'
291 '$$ Record remaining changes to this &file'
291 '$$ Record remaining changes to this &file'
292 '$$ &Done, skip remaining changes and files'
292 '$$ &Done, skip remaining changes and files'
293 '$$ Record &all changes to all remaining files'
293 '$$ Record &all changes to all remaining files'
294 '$$ &Quit, recording no changes'
294 '$$ &Quit, recording no changes'
295 '$$ &?')
295 '$$ &?')
296 r = ui.promptchoice("%s %s" % (query, resps))
296 r = ui.promptchoice("%s %s" % (query, resps))
297 ui.write("\n")
297 ui.write("\n")
298 if r == 8: # ?
298 if r == 8: # ?
299 doc = gettext(record.__doc__)
299 doc = gettext(record.__doc__)
300 c = doc.find('::') + 2
300 c = doc.find('::') + 2
301 for l in doc[c:].splitlines():
301 for l in doc[c:].splitlines():
302 if l.startswith(' '):
302 if l.startswith(' '):
303 ui.write(l.strip(), '\n')
303 ui.write(l.strip(), '\n')
304 continue
304 continue
305 elif r == 0: # yes
305 elif r == 0: # yes
306 ret = True
306 ret = True
307 elif r == 1: # no
307 elif r == 1: # no
308 ret = False
308 ret = False
309 elif r == 2: # Edit patch
309 elif r == 2: # Edit patch
310 if chunk is None:
310 if chunk is None:
311 ui.write(_('cannot edit patch for whole file'))
311 ui.write(_('cannot edit patch for whole file'))
312 ui.write("\n")
312 ui.write("\n")
313 continue
313 continue
314 if chunk.header.binary():
314 if chunk.header.binary():
315 ui.write(_('cannot edit patch for binary file'))
315 ui.write(_('cannot edit patch for binary file'))
316 ui.write("\n")
316 ui.write("\n")
317 continue
317 continue
318 # Patch comment based on the Git one (based on comment at end of
318 # Patch comment based on the Git one (based on comment at end of
319 # http://mercurial.selenic.com/wiki/RecordExtension)
319 # http://mercurial.selenic.com/wiki/RecordExtension)
320 phelp = '---' + _("""
320 phelp = '---' + _("""
321 To remove '-' lines, make them ' ' lines (context).
321 To remove '-' lines, make them ' ' lines (context).
322 To remove '+' lines, delete them.
322 To remove '+' lines, delete them.
323 Lines starting with # will be removed from the patch.
323 Lines starting with # will be removed from the patch.
324
324
325 If the patch applies cleanly, the edited hunk will immediately be
325 If the patch applies cleanly, the edited hunk will immediately be
326 added to the record list. If it does not apply cleanly, a rejects
326 added to the record list. If it does not apply cleanly, a rejects
327 file will be generated: you can use that when you try again. If
327 file will be generated: you can use that when you try again. If
328 all lines of the hunk are removed, then the edit is aborted and
328 all lines of the hunk are removed, then the edit is aborted and
329 the hunk is left unchanged.
329 the hunk is left unchanged.
330 """)
330 """)
331 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
331 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
332 suffix=".diff", text=True)
332 suffix=".diff", text=True)
333 ncpatchfp = None
333 ncpatchfp = None
334 try:
334 try:
335 # Write the initial patch
335 # Write the initial patch
336 f = os.fdopen(patchfd, "w")
336 f = os.fdopen(patchfd, "w")
337 chunk.header.write(f)
337 chunk.header.write(f)
338 chunk.write(f)
338 chunk.write(f)
339 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
339 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
340 f.close()
340 f.close()
341 # Start the editor and wait for it to complete
341 # Start the editor and wait for it to complete
342 editor = ui.geteditor()
342 editor = ui.geteditor()
343 util.system("%s \"%s\"" % (editor, patchfn),
343 util.system("%s \"%s\"" % (editor, patchfn),
344 environ={'HGUSER': ui.username()},
344 environ={'HGUSER': ui.username()},
345 onerr=util.Abort, errprefix=_("edit failed"),
345 onerr=util.Abort, errprefix=_("edit failed"),
346 out=ui.fout)
346 out=ui.fout)
347 # Remove comment lines
347 # Remove comment lines
348 patchfp = open(patchfn)
348 patchfp = open(patchfn)
349 ncpatchfp = cStringIO.StringIO()
349 ncpatchfp = cStringIO.StringIO()
350 for line in patchfp:
350 for line in patchfp:
351 if not line.startswith('#'):
351 if not line.startswith('#'):
352 ncpatchfp.write(line)
352 ncpatchfp.write(line)
353 patchfp.close()
353 patchfp.close()
354 ncpatchfp.seek(0)
354 ncpatchfp.seek(0)
355 newpatches = parsepatch(ncpatchfp)
355 newpatches = parsepatch(ncpatchfp)
356 finally:
356 finally:
357 os.unlink(patchfn)
357 os.unlink(patchfn)
358 del ncpatchfp
358 del ncpatchfp
359 # Signal that the chunk shouldn't be applied as-is, but
359 # Signal that the chunk shouldn't be applied as-is, but
360 # provide the new patch to be used instead.
360 # provide the new patch to be used instead.
361 ret = False
361 ret = False
362 elif r == 3: # Skip
362 elif r == 3: # Skip
363 ret = skipfile = False
363 ret = skipfile = False
364 elif r == 4: # file (Record remaining)
364 elif r == 4: # file (Record remaining)
365 ret = skipfile = True
365 ret = skipfile = True
366 elif r == 5: # done, skip remaining
366 elif r == 5: # done, skip remaining
367 ret = skipall = False
367 ret = skipall = False
368 elif r == 6: # all
368 elif r == 6: # all
369 ret = skipall = True
369 ret = skipall = True
370 elif r == 7: # quit
370 elif r == 7: # quit
371 raise util.Abort(_('user quit'))
371 raise util.Abort(_('user quit'))
372 return ret, skipfile, skipall, newpatches
372 return ret, skipfile, skipall, newpatches
373
373
374 seen = set()
374 seen = set()
375 applied = {} # 'filename' -> [] of chunks
375 applied = {} # 'filename' -> [] of chunks
376 skipfile, skipall = None, None
376 skipfile, skipall = None, None
377 pos, total = 1, sum(len(h.hunks) for h in headers)
377 pos, total = 1, sum(len(h.hunks) for h in headers)
378 for h in headers:
378 for h in headers:
379 pos += len(h.hunks)
379 pos += len(h.hunks)
380 skipfile = None
380 skipfile = None
381 fixoffset = 0
381 fixoffset = 0
382 hdr = ''.join(h.header)
382 hdr = ''.join(h.header)
383 if hdr in seen:
383 if hdr in seen:
384 continue
384 continue
385 seen.add(hdr)
385 seen.add(hdr)
386 if skipall is None:
386 if skipall is None:
387 h.pretty(ui)
387 h.pretty(ui)
388 msg = (_('examine changes to %s?') %
388 msg = (_('examine changes to %s?') %
389 _(' and ').join("'%s'" % f for f in h.files()))
389 _(' and ').join("'%s'" % f for f in h.files()))
390 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
390 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
391 if not r:
391 if not r:
392 continue
392 continue
393 applied[h.filename()] = [h]
393 applied[h.filename()] = [h]
394 if h.allhunks():
394 if h.allhunks():
395 applied[h.filename()] += h.hunks
395 applied[h.filename()] += h.hunks
396 continue
396 continue
397 for i, chunk in enumerate(h.hunks):
397 for i, chunk in enumerate(h.hunks):
398 if skipfile is None and skipall is None:
398 if skipfile is None and skipall is None:
399 chunk.pretty(ui)
399 chunk.pretty(ui)
400 if total == 1:
400 if total == 1:
401 msg = _("record this change to '%s'?") % chunk.filename()
401 msg = _("record this change to '%s'?") % chunk.filename()
402 else:
402 else:
403 idx = pos - len(h.hunks) + i
403 idx = pos - len(h.hunks) + i
404 msg = _("record change %d/%d to '%s'?") % (idx, total,
404 msg = _("record change %d/%d to '%s'?") % (idx, total,
405 chunk.filename())
405 chunk.filename())
406 r, skipfile, skipall, newpatches = prompt(skipfile,
406 r, skipfile, skipall, newpatches = prompt(skipfile,
407 skipall, msg, chunk)
407 skipall, msg, chunk)
408 if r:
408 if r:
409 if fixoffset:
409 if fixoffset:
410 chunk = copy.copy(chunk)
410 chunk = copy.copy(chunk)
411 chunk.toline += fixoffset
411 chunk.toline += fixoffset
412 applied[chunk.filename()].append(chunk)
412 applied[chunk.filename()].append(chunk)
413 elif newpatches is not None:
413 elif newpatches is not None:
414 for newpatch in newpatches:
414 for newpatch in newpatches:
415 for newhunk in newpatch.hunks:
415 for newhunk in newpatch.hunks:
416 if fixoffset:
416 if fixoffset:
417 newhunk.toline += fixoffset
417 newhunk.toline += fixoffset
418 applied[newhunk.filename()].append(newhunk)
418 applied[newhunk.filename()].append(newhunk)
419 else:
419 else:
420 fixoffset += chunk.removed - chunk.added
420 fixoffset += chunk.removed - chunk.added
421 return sum([h for h in applied.itervalues()
421 return sum([h for h in applied.itervalues()
422 if h[0].special() or len(h) > 1], [])
422 if h[0].special() or len(h) > 1], [])
423
423
424 @command("record",
424 @command("record",
425 # same options as commit + white space diff options
425 # same options as commit + white space diff options
426 commands.table['^commit|ci'][1][:] + diffopts,
426 commands.table['^commit|ci'][1][:] + diffopts,
427 _('hg record [OPTION]... [FILE]...'))
427 _('hg record [OPTION]... [FILE]...'))
428 def record(ui, repo, *pats, **opts):
428 def record(ui, repo, *pats, **opts):
429 '''interactively select changes to commit
429 '''interactively select changes to commit
430
430
431 If a list of files is omitted, all changes reported by :hg:`status`
431 If a list of files is omitted, all changes reported by :hg:`status`
432 will be candidates for recording.
432 will be candidates for recording.
433
433
434 See :hg:`help dates` for a list of formats valid for -d/--date.
434 See :hg:`help dates` for a list of formats valid for -d/--date.
435
435
436 You will be prompted for whether to record changes to each
436 You will be prompted for whether to record changes to each
437 modified file, and for files with multiple changes, for each
437 modified file, and for files with multiple changes, for each
438 change to use. For each query, the following responses are
438 change to use. For each query, the following responses are
439 possible::
439 possible::
440
440
441 y - record this change
441 y - record this change
442 n - skip this change
442 n - skip this change
443 e - edit this change manually
443 e - edit this change manually
444
444
445 s - skip remaining changes to this file
445 s - skip remaining changes to this file
446 f - record remaining changes to this file
446 f - record remaining changes to this file
447
447
448 d - done, skip remaining changes and files
448 d - done, skip remaining changes and files
449 a - record all changes to all remaining files
449 a - record all changes to all remaining files
450 q - quit, recording no changes
450 q - quit, recording no changes
451
451
452 ? - display help
452 ? - display help
453
453
454 This command is not available when committing a merge.'''
454 This command is not available when committing a merge.'''
455
455
456 dorecord(ui, repo, commands.commit, 'commit', False, *pats, **opts)
456 dorecord(ui, repo, commands.commit, 'commit', False, *pats, **opts)
457
457
458 def qrefresh(origfn, ui, repo, *pats, **opts):
458 def qrefresh(origfn, ui, repo, *pats, **opts):
459 if not opts['interactive']:
459 if not opts['interactive']:
460 return origfn(ui, repo, *pats, **opts)
460 return origfn(ui, repo, *pats, **opts)
461
461
462 mq = extensions.find('mq')
462 mq = extensions.find('mq')
463
463
464 def committomq(ui, repo, *pats, **opts):
464 def committomq(ui, repo, *pats, **opts):
465 # At this point the working copy contains only changes that
465 # At this point the working copy contains only changes that
466 # were accepted. All other changes were reverted.
466 # were accepted. All other changes were reverted.
467 # We can't pass *pats here since qrefresh will undo all other
467 # We can't pass *pats here since qrefresh will undo all other
468 # changed files in the patch that aren't in pats.
468 # changed files in the patch that aren't in pats.
469 mq.refresh(ui, repo, **opts)
469 mq.refresh(ui, repo, **opts)
470
470
471 # backup all changed files
471 # backup all changed files
472 dorecord(ui, repo, committomq, 'qrefresh', True, *pats, **opts)
472 dorecord(ui, repo, committomq, 'qrefresh', True, *pats, **opts)
473
473
474 def qrecord(ui, repo, patch, *pats, **opts):
474 def qrecord(ui, repo, patch, *pats, **opts):
475 '''interactively record a new patch
475 '''interactively record a new patch
476
476
477 See :hg:`help qnew` & :hg:`help record` for more information and
477 See :hg:`help qnew` & :hg:`help record` for more information and
478 usage.
478 usage.
479 '''
479 '''
480
480
481 try:
481 try:
482 mq = extensions.find('mq')
482 mq = extensions.find('mq')
483 except KeyError:
483 except KeyError:
484 raise util.Abort(_("'mq' extension not loaded"))
484 raise util.Abort(_("'mq' extension not loaded"))
485
485
486 repo.mq.checkpatchname(patch)
486 repo.mq.checkpatchname(patch)
487
487
488 def committomq(ui, repo, *pats, **opts):
488 def committomq(ui, repo, *pats, **opts):
489 opts['checkname'] = False
489 opts['checkname'] = False
490 mq.new(ui, repo, patch, *pats, **opts)
490 mq.new(ui, repo, patch, *pats, **opts)
491
491
492 dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts)
492 dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts)
493
493
494 def qnew(origfn, ui, repo, patch, *args, **opts):
494 def qnew(origfn, ui, repo, patch, *args, **opts):
495 if opts['interactive']:
495 if opts['interactive']:
496 return qrecord(ui, repo, patch, *args, **opts)
496 return qrecord(ui, repo, patch, *args, **opts)
497 return origfn(ui, repo, patch, *args, **opts)
497 return origfn(ui, repo, patch, *args, **opts)
498
498
499 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts):
499 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts):
500 if not ui.interactive():
500 if not ui.interactive():
501 raise util.Abort(_('running non-interactively, use %s instead') %
501 raise util.Abort(_('running non-interactively, use %s instead') %
502 cmdsuggest)
502 cmdsuggest)
503
503
504 # make sure username is set before going interactive
504 # make sure username is set before going interactive
505 ui.username()
505 ui.username()
506
506
507 def recordfunc(ui, repo, message, match, opts):
507 def recordfunc(ui, repo, message, match, opts):
508 """This is generic record driver.
508 """This is generic record driver.
509
509
510 Its job is to interactively filter local changes, and
510 Its job is to interactively filter local changes, and
511 accordingly prepare working directory into a state in which the
511 accordingly prepare working directory into a state in which the
512 job can be delegated to a non-interactive commit command such as
512 job can be delegated to a non-interactive commit command such as
513 'commit' or 'qrefresh'.
513 'commit' or 'qrefresh'.
514
514
515 After the actual job is done by non-interactive command, the
515 After the actual job is done by non-interactive command, the
516 working directory is restored to its original state.
516 working directory is restored to its original state.
517
517
518 In the end we'll record interesting changes, and everything else
518 In the end we'll record interesting changes, and everything else
519 will be left in place, so the user can continue working.
519 will be left in place, so the user can continue working.
520 """
520 """
521
521
522 cmdutil.checkunfinished(repo, commit=True)
522 merge = len(repo[None].parents()) > 1
523 merge = len(repo[None].parents()) > 1
523 if merge:
524 if merge:
524 raise util.Abort(_('cannot partially commit a merge '
525 raise util.Abort(_('cannot partially commit a merge '
525 '(use "hg commit" instead)'))
526 '(use "hg commit" instead)'))
526
527
527 changes = repo.status(match=match)[:3]
528 changes = repo.status(match=match)[:3]
528 diffopts = patch.diffopts(ui, opts=dict(
529 diffopts = patch.diffopts(ui, opts=dict(
529 git=True, nodates=True,
530 git=True, nodates=True,
530 ignorews=opts.get('ignore_all_space'),
531 ignorews=opts.get('ignore_all_space'),
531 ignorewsamount=opts.get('ignore_space_change'),
532 ignorewsamount=opts.get('ignore_space_change'),
532 ignoreblanklines=opts.get('ignore_blank_lines')))
533 ignoreblanklines=opts.get('ignore_blank_lines')))
533 chunks = patch.diff(repo, changes=changes, opts=diffopts)
534 chunks = patch.diff(repo, changes=changes, opts=diffopts)
534 fp = cStringIO.StringIO()
535 fp = cStringIO.StringIO()
535 fp.write(''.join(chunks))
536 fp.write(''.join(chunks))
536 fp.seek(0)
537 fp.seek(0)
537
538
538 # 1. filter patch, so we have intending-to apply subset of it
539 # 1. filter patch, so we have intending-to apply subset of it
539 try:
540 try:
540 chunks = filterpatch(ui, parsepatch(fp))
541 chunks = filterpatch(ui, parsepatch(fp))
541 except patch.PatchError, err:
542 except patch.PatchError, err:
542 raise util.Abort(_('error parsing patch: %s') % err)
543 raise util.Abort(_('error parsing patch: %s') % err)
543
544
544 del fp
545 del fp
545
546
546 contenders = set()
547 contenders = set()
547 for h in chunks:
548 for h in chunks:
548 try:
549 try:
549 contenders.update(set(h.files()))
550 contenders.update(set(h.files()))
550 except AttributeError:
551 except AttributeError:
551 pass
552 pass
552
553
553 changed = changes[0] + changes[1] + changes[2]
554 changed = changes[0] + changes[1] + changes[2]
554 newfiles = [f for f in changed if f in contenders]
555 newfiles = [f for f in changed if f in contenders]
555 if not newfiles:
556 if not newfiles:
556 ui.status(_('no changes to record\n'))
557 ui.status(_('no changes to record\n'))
557 return 0
558 return 0
558
559
559 modified = set(changes[0])
560 modified = set(changes[0])
560
561
561 # 2. backup changed files, so we can restore them in the end
562 # 2. backup changed files, so we can restore them in the end
562 if backupall:
563 if backupall:
563 tobackup = changed
564 tobackup = changed
564 else:
565 else:
565 tobackup = [f for f in newfiles if f in modified]
566 tobackup = [f for f in newfiles if f in modified]
566
567
567 backups = {}
568 backups = {}
568 if tobackup:
569 if tobackup:
569 backupdir = repo.join('record-backups')
570 backupdir = repo.join('record-backups')
570 try:
571 try:
571 os.mkdir(backupdir)
572 os.mkdir(backupdir)
572 except OSError, err:
573 except OSError, err:
573 if err.errno != errno.EEXIST:
574 if err.errno != errno.EEXIST:
574 raise
575 raise
575 try:
576 try:
576 # backup continues
577 # backup continues
577 for f in tobackup:
578 for f in tobackup:
578 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
579 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
579 dir=backupdir)
580 dir=backupdir)
580 os.close(fd)
581 os.close(fd)
581 ui.debug('backup %r as %r\n' % (f, tmpname))
582 ui.debug('backup %r as %r\n' % (f, tmpname))
582 util.copyfile(repo.wjoin(f), tmpname)
583 util.copyfile(repo.wjoin(f), tmpname)
583 shutil.copystat(repo.wjoin(f), tmpname)
584 shutil.copystat(repo.wjoin(f), tmpname)
584 backups[f] = tmpname
585 backups[f] = tmpname
585
586
586 fp = cStringIO.StringIO()
587 fp = cStringIO.StringIO()
587 for c in chunks:
588 for c in chunks:
588 if c.filename() in backups:
589 if c.filename() in backups:
589 c.write(fp)
590 c.write(fp)
590 dopatch = fp.tell()
591 dopatch = fp.tell()
591 fp.seek(0)
592 fp.seek(0)
592
593
593 # 3a. apply filtered patch to clean repo (clean)
594 # 3a. apply filtered patch to clean repo (clean)
594 if backups:
595 if backups:
595 hg.revert(repo, repo.dirstate.p1(),
596 hg.revert(repo, repo.dirstate.p1(),
596 lambda key: key in backups)
597 lambda key: key in backups)
597
598
598 # 3b. (apply)
599 # 3b. (apply)
599 if dopatch:
600 if dopatch:
600 try:
601 try:
601 ui.debug('applying patch\n')
602 ui.debug('applying patch\n')
602 ui.debug(fp.getvalue())
603 ui.debug(fp.getvalue())
603 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
604 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
604 except patch.PatchError, err:
605 except patch.PatchError, err:
605 raise util.Abort(str(err))
606 raise util.Abort(str(err))
606 del fp
607 del fp
607
608
608 # 4. We prepared working directory according to filtered
609 # 4. We prepared working directory according to filtered
609 # patch. Now is the time to delegate the job to
610 # patch. Now is the time to delegate the job to
610 # commit/qrefresh or the like!
611 # commit/qrefresh or the like!
611
612
612 # it is important to first chdir to repo root -- we'll call
613 # it is important to first chdir to repo root -- we'll call
613 # a highlevel command with list of pathnames relative to
614 # a highlevel command with list of pathnames relative to
614 # repo root
615 # repo root
615 cwd = os.getcwd()
616 cwd = os.getcwd()
616 os.chdir(repo.root)
617 os.chdir(repo.root)
617 try:
618 try:
618 commitfunc(ui, repo, *newfiles, **opts)
619 commitfunc(ui, repo, *newfiles, **opts)
619 finally:
620 finally:
620 os.chdir(cwd)
621 os.chdir(cwd)
621
622
622 return 0
623 return 0
623 finally:
624 finally:
624 # 5. finally restore backed-up files
625 # 5. finally restore backed-up files
625 try:
626 try:
626 for realname, tmpname in backups.iteritems():
627 for realname, tmpname in backups.iteritems():
627 ui.debug('restoring %r to %r\n' % (tmpname, realname))
628 ui.debug('restoring %r to %r\n' % (tmpname, realname))
628 util.copyfile(tmpname, repo.wjoin(realname))
629 util.copyfile(tmpname, repo.wjoin(realname))
629 # Our calls to copystat() here and above are a
630 # Our calls to copystat() here and above are a
630 # hack to trick any editors that have f open that
631 # hack to trick any editors that have f open that
631 # we haven't modified them.
632 # we haven't modified them.
632 #
633 #
633 # Also note that this racy as an editor could
634 # Also note that this racy as an editor could
634 # notice the file's mtime before we've finished
635 # notice the file's mtime before we've finished
635 # writing it.
636 # writing it.
636 shutil.copystat(tmpname, repo.wjoin(realname))
637 shutil.copystat(tmpname, repo.wjoin(realname))
637 os.unlink(tmpname)
638 os.unlink(tmpname)
638 if tobackup:
639 if tobackup:
639 os.rmdir(backupdir)
640 os.rmdir(backupdir)
640 except OSError:
641 except OSError:
641 pass
642 pass
642
643
643 # wrap ui.write so diff output can be labeled/colorized
644 # wrap ui.write so diff output can be labeled/colorized
644 def wrapwrite(orig, *args, **kw):
645 def wrapwrite(orig, *args, **kw):
645 label = kw.pop('label', '')
646 label = kw.pop('label', '')
646 for chunk, l in patch.difflabel(lambda: args):
647 for chunk, l in patch.difflabel(lambda: args):
647 orig(chunk, label=label + l)
648 orig(chunk, label=label + l)
648 oldwrite = ui.write
649 oldwrite = ui.write
649 extensions.wrapfunction(ui, 'write', wrapwrite)
650 extensions.wrapfunction(ui, 'write', wrapwrite)
650 try:
651 try:
651 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
652 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
652 finally:
653 finally:
653 ui.write = oldwrite
654 ui.write = oldwrite
654
655
655 cmdtable["qrecord"] = \
656 cmdtable["qrecord"] = \
656 (qrecord, [], # placeholder until mq is available
657 (qrecord, [], # placeholder until mq is available
657 _('hg qrecord [OPTION]... PATCH [FILE]...'))
658 _('hg qrecord [OPTION]... PATCH [FILE]...'))
658
659
659 def uisetup(ui):
660 def uisetup(ui):
660 try:
661 try:
661 mq = extensions.find('mq')
662 mq = extensions.find('mq')
662 except KeyError:
663 except KeyError:
663 return
664 return
664
665
665 cmdtable["qrecord"] = \
666 cmdtable["qrecord"] = \
666 (qrecord,
667 (qrecord,
667 # same options as qnew, but copy them so we don't get
668 # same options as qnew, but copy them so we don't get
668 # -i/--interactive for qrecord and add white space diff options
669 # -i/--interactive for qrecord and add white space diff options
669 mq.cmdtable['^qnew'][1][:] + diffopts,
670 mq.cmdtable['^qnew'][1][:] + diffopts,
670 _('hg qrecord [OPTION]... PATCH [FILE]...'))
671 _('hg qrecord [OPTION]... PATCH [FILE]...'))
671
672
672 _wrapcmd('qnew', mq.cmdtable, qnew, _("interactively record a new patch"))
673 _wrapcmd('qnew', mq.cmdtable, qnew, _("interactively record a new patch"))
673 _wrapcmd('qrefresh', mq.cmdtable, qrefresh,
674 _wrapcmd('qrefresh', mq.cmdtable, qrefresh,
674 _("interactively select changes to refresh"))
675 _("interactively select changes to refresh"))
675
676
676 def _wrapcmd(cmd, table, wrapfn, msg):
677 def _wrapcmd(cmd, table, wrapfn, msg):
677 entry = extensions.wrapcommand(table, cmd, wrapfn)
678 entry = extensions.wrapcommand(table, cmd, wrapfn)
678 entry[1].append(('i', 'interactive', None, msg))
679 entry[1].append(('i', 'interactive', None, msg))
679
680
680 commands.inferrepo += " record qrecord"
681 commands.inferrepo += " record qrecord"
General Comments 0
You need to be logged in to leave comments. Login now