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