##// END OF EJS Templates
record: clean up command table...
Kevin Bullock -
r13196:592998ba default
parent child Browse files
Show More
@@ -1,586 +1,576 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, mdiff, patch
11 from mercurial import cmdutil, commands, extensions, hg, mdiff, 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 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16
16
17 def scanpatch(fp):
17 def scanpatch(fp):
18 """like patch.iterhunks, but yield different events
18 """like patch.iterhunks, but yield different events
19
19
20 - ('file', [header_lines + fromfile + tofile])
20 - ('file', [header_lines + fromfile + tofile])
21 - ('context', [context_lines])
21 - ('context', [context_lines])
22 - ('hunk', [hunk_lines])
22 - ('hunk', [hunk_lines])
23 - ('range', (-start,len, +start,len, diffp))
23 - ('range', (-start,len, +start,len, diffp))
24 """
24 """
25 lr = patch.linereader(fp)
25 lr = patch.linereader(fp)
26
26
27 def scanwhile(first, p):
27 def scanwhile(first, p):
28 """scan lr while predicate holds"""
28 """scan lr while predicate holds"""
29 lines = [first]
29 lines = [first]
30 while True:
30 while True:
31 line = lr.readline()
31 line = lr.readline()
32 if not line:
32 if not line:
33 break
33 break
34 if p(line):
34 if p(line):
35 lines.append(line)
35 lines.append(line)
36 else:
36 else:
37 lr.push(line)
37 lr.push(line)
38 break
38 break
39 return lines
39 return lines
40
40
41 while True:
41 while True:
42 line = lr.readline()
42 line = lr.readline()
43 if not line:
43 if not line:
44 break
44 break
45 if line.startswith('diff --git a/') or line.startswith('diff -r '):
45 if line.startswith('diff --git a/') or line.startswith('diff -r '):
46 def notheader(line):
46 def notheader(line):
47 s = line.split(None, 1)
47 s = line.split(None, 1)
48 return not s or s[0] not in ('---', 'diff')
48 return not s or s[0] not in ('---', 'diff')
49 header = scanwhile(line, notheader)
49 header = scanwhile(line, notheader)
50 fromfile = lr.readline()
50 fromfile = lr.readline()
51 if fromfile.startswith('---'):
51 if fromfile.startswith('---'):
52 tofile = lr.readline()
52 tofile = lr.readline()
53 header += [fromfile, tofile]
53 header += [fromfile, tofile]
54 else:
54 else:
55 lr.push(fromfile)
55 lr.push(fromfile)
56 yield 'file', header
56 yield 'file', header
57 elif line[0] == ' ':
57 elif line[0] == ' ':
58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
59 elif line[0] in '-+':
59 elif line[0] in '-+':
60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
61 else:
61 else:
62 m = lines_re.match(line)
62 m = lines_re.match(line)
63 if m:
63 if m:
64 yield 'range', m.groups()
64 yield 'range', m.groups()
65 else:
65 else:
66 raise patch.PatchError('unknown patch content: %r' % line)
66 raise patch.PatchError('unknown patch content: %r' % line)
67
67
68 class header(object):
68 class header(object):
69 """patch header
69 """patch header
70
70
71 XXX shoudn't we move this to mercurial/patch.py ?
71 XXX shoudn't we move this to mercurial/patch.py ?
72 """
72 """
73 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
73 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
74 diff_re = re.compile('diff -r .* (.*)$')
74 diff_re = re.compile('diff -r .* (.*)$')
75 allhunks_re = re.compile('(?:index|new file|deleted file) ')
75 allhunks_re = re.compile('(?:index|new file|deleted file) ')
76 pretty_re = re.compile('(?:new file|deleted file) ')
76 pretty_re = re.compile('(?:new file|deleted file) ')
77 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
77 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
78
78
79 def __init__(self, header):
79 def __init__(self, header):
80 self.header = header
80 self.header = header
81 self.hunks = []
81 self.hunks = []
82
82
83 def binary(self):
83 def binary(self):
84 for h in self.header:
84 for h in self.header:
85 if h.startswith('index '):
85 if h.startswith('index '):
86 return True
86 return True
87
87
88 def pretty(self, fp):
88 def pretty(self, fp):
89 for h in self.header:
89 for h in self.header:
90 if h.startswith('index '):
90 if h.startswith('index '):
91 fp.write(_('this modifies a binary file (all or nothing)\n'))
91 fp.write(_('this modifies a binary file (all or nothing)\n'))
92 break
92 break
93 if self.pretty_re.match(h):
93 if self.pretty_re.match(h):
94 fp.write(h)
94 fp.write(h)
95 if self.binary():
95 if self.binary():
96 fp.write(_('this is a binary file\n'))
96 fp.write(_('this is a binary file\n'))
97 break
97 break
98 if h.startswith('---'):
98 if h.startswith('---'):
99 fp.write(_('%d hunks, %d lines changed\n') %
99 fp.write(_('%d hunks, %d lines changed\n') %
100 (len(self.hunks),
100 (len(self.hunks),
101 sum([max(h.added, h.removed) for h in self.hunks])))
101 sum([max(h.added, h.removed) for h in self.hunks])))
102 break
102 break
103 fp.write(h)
103 fp.write(h)
104
104
105 def write(self, fp):
105 def write(self, fp):
106 fp.write(''.join(self.header))
106 fp.write(''.join(self.header))
107
107
108 def allhunks(self):
108 def allhunks(self):
109 for h in self.header:
109 for h in self.header:
110 if self.allhunks_re.match(h):
110 if self.allhunks_re.match(h):
111 return True
111 return True
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 for h in self.header:
130 for h in self.header:
131 if self.special_re.match(h):
131 if self.special_re.match(h):
132 return True
132 return True
133
133
134 def countchanges(hunk):
134 def countchanges(hunk):
135 """hunk -> (n+,n-)"""
135 """hunk -> (n+,n-)"""
136 add = len([h for h in hunk if h[0] == '+'])
136 add = len([h for h in hunk if h[0] == '+'])
137 rem = len([h for h in hunk if h[0] == '-'])
137 rem = len([h for h in hunk if h[0] == '-'])
138 return add, rem
138 return add, rem
139
139
140 class hunk(object):
140 class hunk(object):
141 """patch hunk
141 """patch hunk
142
142
143 XXX shouldn't we merge this with patch.hunk ?
143 XXX shouldn't we merge this with patch.hunk ?
144 """
144 """
145 maxcontext = 3
145 maxcontext = 3
146
146
147 def __init__(self, header, fromline, toline, proc, before, hunk, after):
147 def __init__(self, header, fromline, toline, proc, before, hunk, after):
148 def trimcontext(number, lines):
148 def trimcontext(number, lines):
149 delta = len(lines) - self.maxcontext
149 delta = len(lines) - self.maxcontext
150 if False and delta > 0:
150 if False and delta > 0:
151 return number + delta, lines[:self.maxcontext]
151 return number + delta, lines[:self.maxcontext]
152 return number, lines
152 return number, lines
153
153
154 self.header = header
154 self.header = header
155 self.fromline, self.before = trimcontext(fromline, before)
155 self.fromline, self.before = trimcontext(fromline, before)
156 self.toline, self.after = trimcontext(toline, after)
156 self.toline, self.after = trimcontext(toline, after)
157 self.proc = proc
157 self.proc = proc
158 self.hunk = hunk
158 self.hunk = hunk
159 self.added, self.removed = countchanges(self.hunk)
159 self.added, self.removed = countchanges(self.hunk)
160
160
161 def write(self, fp):
161 def write(self, fp):
162 delta = len(self.before) + len(self.after)
162 delta = len(self.before) + len(self.after)
163 if self.after and self.after[-1] == '\\ No newline at end of file\n':
163 if self.after and self.after[-1] == '\\ No newline at end of file\n':
164 delta -= 1
164 delta -= 1
165 fromlen = delta + self.removed
165 fromlen = delta + self.removed
166 tolen = delta + self.added
166 tolen = delta + self.added
167 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
167 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
168 (self.fromline, fromlen, self.toline, tolen,
168 (self.fromline, fromlen, self.toline, tolen,
169 self.proc and (' ' + self.proc)))
169 self.proc and (' ' + self.proc)))
170 fp.write(''.join(self.before + self.hunk + self.after))
170 fp.write(''.join(self.before + self.hunk + self.after))
171
171
172 pretty = write
172 pretty = write
173
173
174 def filename(self):
174 def filename(self):
175 return self.header.filename()
175 return self.header.filename()
176
176
177 def __repr__(self):
177 def __repr__(self):
178 return '<hunk %r@%d>' % (self.filename(), self.fromline)
178 return '<hunk %r@%d>' % (self.filename(), self.fromline)
179
179
180 def parsepatch(fp):
180 def parsepatch(fp):
181 """patch -> [] of hunks """
181 """patch -> [] of hunks """
182 class parser(object):
182 class parser(object):
183 """patch parsing state machine"""
183 """patch parsing state machine"""
184 def __init__(self):
184 def __init__(self):
185 self.fromline = 0
185 self.fromline = 0
186 self.toline = 0
186 self.toline = 0
187 self.proc = ''
187 self.proc = ''
188 self.header = None
188 self.header = None
189 self.context = []
189 self.context = []
190 self.before = []
190 self.before = []
191 self.hunk = []
191 self.hunk = []
192 self.stream = []
192 self.stream = []
193
193
194 def addrange(self, limits):
194 def addrange(self, limits):
195 fromstart, fromend, tostart, toend, proc = limits
195 fromstart, fromend, tostart, toend, proc = limits
196 self.fromline = int(fromstart)
196 self.fromline = int(fromstart)
197 self.toline = int(tostart)
197 self.toline = int(tostart)
198 self.proc = proc
198 self.proc = proc
199
199
200 def addcontext(self, context):
200 def addcontext(self, context):
201 if self.hunk:
201 if self.hunk:
202 h = hunk(self.header, self.fromline, self.toline, self.proc,
202 h = hunk(self.header, self.fromline, self.toline, self.proc,
203 self.before, self.hunk, context)
203 self.before, self.hunk, context)
204 self.header.hunks.append(h)
204 self.header.hunks.append(h)
205 self.stream.append(h)
205 self.stream.append(h)
206 self.fromline += len(self.before) + h.removed
206 self.fromline += len(self.before) + h.removed
207 self.toline += len(self.before) + h.added
207 self.toline += len(self.before) + h.added
208 self.before = []
208 self.before = []
209 self.hunk = []
209 self.hunk = []
210 self.proc = ''
210 self.proc = ''
211 self.context = context
211 self.context = context
212
212
213 def addhunk(self, hunk):
213 def addhunk(self, hunk):
214 if self.context:
214 if self.context:
215 self.before = self.context
215 self.before = self.context
216 self.context = []
216 self.context = []
217 self.hunk = hunk
217 self.hunk = hunk
218
218
219 def newfile(self, hdr):
219 def newfile(self, hdr):
220 self.addcontext([])
220 self.addcontext([])
221 h = header(hdr)
221 h = header(hdr)
222 self.stream.append(h)
222 self.stream.append(h)
223 self.header = h
223 self.header = h
224
224
225 def finished(self):
225 def finished(self):
226 self.addcontext([])
226 self.addcontext([])
227 return self.stream
227 return self.stream
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 'hunk': {'context': addcontext,
237 'hunk': {'context': addcontext,
238 'file': newfile,
238 'file': newfile,
239 'range': addrange},
239 'range': addrange},
240 'range': {'context': addcontext,
240 'range': {'context': addcontext,
241 'hunk': addhunk},
241 'hunk': addhunk},
242 }
242 }
243
243
244 p = parser()
244 p = parser()
245
245
246 state = 'context'
246 state = 'context'
247 for newstate, data in scanpatch(fp):
247 for newstate, data in scanpatch(fp):
248 try:
248 try:
249 p.transitions[state][newstate](p, data)
249 p.transitions[state][newstate](p, data)
250 except KeyError:
250 except KeyError:
251 raise patch.PatchError('unhandled transition: %s -> %s' %
251 raise patch.PatchError('unhandled transition: %s -> %s' %
252 (state, newstate))
252 (state, newstate))
253 state = newstate
253 state = newstate
254 return p.finished()
254 return p.finished()
255
255
256 def filterpatch(ui, chunks):
256 def filterpatch(ui, chunks):
257 """Interactively filter patch chunks into applied-only chunks"""
257 """Interactively filter patch chunks into applied-only chunks"""
258 chunks = list(chunks)
258 chunks = list(chunks)
259 chunks.reverse()
259 chunks.reverse()
260 seen = set()
260 seen = set()
261 def consumefile():
261 def consumefile():
262 """fetch next portion from chunks until a 'header' is seen
262 """fetch next portion from chunks until a 'header' is seen
263 NB: header == new-file mark
263 NB: header == new-file mark
264 """
264 """
265 consumed = []
265 consumed = []
266 while chunks:
266 while chunks:
267 if isinstance(chunks[-1], header):
267 if isinstance(chunks[-1], header):
268 break
268 break
269 else:
269 else:
270 consumed.append(chunks.pop())
270 consumed.append(chunks.pop())
271 return consumed
271 return consumed
272
272
273 resp_all = [None] # this two are changed from inside prompt,
273 resp_all = [None] # this two are changed from inside prompt,
274 resp_file = [None] # so can't be usual variables
274 resp_file = [None] # so can't be usual variables
275 applied = {} # 'filename' -> [] of chunks
275 applied = {} # 'filename' -> [] of chunks
276 def prompt(query):
276 def prompt(query):
277 """prompt query, and process base inputs
277 """prompt query, and process base inputs
278
278
279 - y/n for the rest of file
279 - y/n for the rest of file
280 - y/n for the rest
280 - y/n for the rest
281 - ? (help)
281 - ? (help)
282 - q (quit)
282 - q (quit)
283
283
284 Returns True/False and sets reps_all and resp_file as
284 Returns True/False and sets reps_all and resp_file as
285 appropriate.
285 appropriate.
286 """
286 """
287 if resp_all[0] is not None:
287 if resp_all[0] is not None:
288 return resp_all[0]
288 return resp_all[0]
289 if resp_file[0] is not None:
289 if resp_file[0] is not None:
290 return resp_file[0]
290 return resp_file[0]
291 while True:
291 while True:
292 resps = _('[Ynsfdaq?]')
292 resps = _('[Ynsfdaq?]')
293 choices = (_('&Yes, record this change'),
293 choices = (_('&Yes, record this change'),
294 _('&No, skip this change'),
294 _('&No, skip this change'),
295 _('&Skip remaining changes to this file'),
295 _('&Skip remaining changes to this file'),
296 _('Record remaining changes to this &file'),
296 _('Record remaining changes to this &file'),
297 _('&Done, skip remaining changes and files'),
297 _('&Done, skip remaining changes and files'),
298 _('Record &all changes to all remaining files'),
298 _('Record &all changes to all remaining files'),
299 _('&Quit, recording no changes'),
299 _('&Quit, recording no changes'),
300 _('&?'))
300 _('&?'))
301 r = ui.promptchoice("%s %s" % (query, resps), choices)
301 r = ui.promptchoice("%s %s" % (query, resps), choices)
302 ui.write("\n")
302 ui.write("\n")
303 if r == 7: # ?
303 if r == 7: # ?
304 doc = gettext(record.__doc__)
304 doc = gettext(record.__doc__)
305 c = doc.find('::') + 2
305 c = doc.find('::') + 2
306 for l in doc[c:].splitlines():
306 for l in doc[c:].splitlines():
307 if l.startswith(' '):
307 if l.startswith(' '):
308 ui.write(l.strip(), '\n')
308 ui.write(l.strip(), '\n')
309 continue
309 continue
310 elif r == 0: # yes
310 elif r == 0: # yes
311 ret = True
311 ret = True
312 elif r == 1: # no
312 elif r == 1: # no
313 ret = False
313 ret = False
314 elif r == 2: # Skip
314 elif r == 2: # Skip
315 ret = resp_file[0] = False
315 ret = resp_file[0] = False
316 elif r == 3: # file (Record remaining)
316 elif r == 3: # file (Record remaining)
317 ret = resp_file[0] = True
317 ret = resp_file[0] = True
318 elif r == 4: # done, skip remaining
318 elif r == 4: # done, skip remaining
319 ret = resp_all[0] = False
319 ret = resp_all[0] = False
320 elif r == 5: # all
320 elif r == 5: # all
321 ret = resp_all[0] = True
321 ret = resp_all[0] = True
322 elif r == 6: # quit
322 elif r == 6: # quit
323 raise util.Abort(_('user quit'))
323 raise util.Abort(_('user quit'))
324 return ret
324 return ret
325 pos, total = 0, len(chunks) - 1
325 pos, total = 0, len(chunks) - 1
326 while chunks:
326 while chunks:
327 pos = total - len(chunks) + 1
327 pos = total - len(chunks) + 1
328 chunk = chunks.pop()
328 chunk = chunks.pop()
329 if isinstance(chunk, header):
329 if isinstance(chunk, header):
330 # new-file mark
330 # new-file mark
331 resp_file = [None]
331 resp_file = [None]
332 fixoffset = 0
332 fixoffset = 0
333 hdr = ''.join(chunk.header)
333 hdr = ''.join(chunk.header)
334 if hdr in seen:
334 if hdr in seen:
335 consumefile()
335 consumefile()
336 continue
336 continue
337 seen.add(hdr)
337 seen.add(hdr)
338 if resp_all[0] is None:
338 if resp_all[0] is None:
339 chunk.pretty(ui)
339 chunk.pretty(ui)
340 r = prompt(_('examine changes to %s?') %
340 r = prompt(_('examine changes to %s?') %
341 _(' and ').join(map(repr, chunk.files())))
341 _(' and ').join(map(repr, chunk.files())))
342 if r:
342 if r:
343 applied[chunk.filename()] = [chunk]
343 applied[chunk.filename()] = [chunk]
344 if chunk.allhunks():
344 if chunk.allhunks():
345 applied[chunk.filename()] += consumefile()
345 applied[chunk.filename()] += consumefile()
346 else:
346 else:
347 consumefile()
347 consumefile()
348 else:
348 else:
349 # new hunk
349 # new hunk
350 if resp_file[0] is None and resp_all[0] is None:
350 if resp_file[0] is None and resp_all[0] is None:
351 chunk.pretty(ui)
351 chunk.pretty(ui)
352 r = (total == 1
352 r = (total == 1
353 and prompt(_('record this change to %r?') % chunk.filename())
353 and prompt(_('record this change to %r?') % chunk.filename())
354 or prompt(_('record change %d/%d to %r?') %
354 or prompt(_('record change %d/%d to %r?') %
355 (pos, total, chunk.filename())))
355 (pos, total, chunk.filename())))
356 if r:
356 if r:
357 if fixoffset:
357 if fixoffset:
358 chunk = copy.copy(chunk)
358 chunk = copy.copy(chunk)
359 chunk.toline += fixoffset
359 chunk.toline += fixoffset
360 applied[chunk.filename()].append(chunk)
360 applied[chunk.filename()].append(chunk)
361 else:
361 else:
362 fixoffset += chunk.removed - chunk.added
362 fixoffset += chunk.removed - chunk.added
363 return sum([h for h in applied.itervalues()
363 return sum([h for h in applied.itervalues()
364 if h[0].special() or len(h) > 1], [])
364 if h[0].special() or len(h) > 1], [])
365
365
366 def record(ui, repo, *pats, **opts):
366 def record(ui, repo, *pats, **opts):
367 '''interactively select changes to commit
367 '''interactively select changes to commit
368
368
369 If a list of files is omitted, all changes reported by :hg:`status`
369 If a list of files is omitted, all changes reported by :hg:`status`
370 will be candidates for recording.
370 will be candidates for recording.
371
371
372 See :hg:`help dates` for a list of formats valid for -d/--date.
372 See :hg:`help dates` for a list of formats valid for -d/--date.
373
373
374 You will be prompted for whether to record changes to each
374 You will be prompted for whether to record changes to each
375 modified file, and for files with multiple changes, for each
375 modified file, and for files with multiple changes, for each
376 change to use. For each query, the following responses are
376 change to use. For each query, the following responses are
377 possible::
377 possible::
378
378
379 y - record this change
379 y - record this change
380 n - skip this change
380 n - skip this change
381
381
382 s - skip remaining changes to this file
382 s - skip remaining changes to this file
383 f - record remaining changes to this file
383 f - record remaining changes to this file
384
384
385 d - done, skip remaining changes and files
385 d - done, skip remaining changes and files
386 a - record all changes to all remaining files
386 a - record all changes to all remaining files
387 q - quit, recording no changes
387 q - quit, recording no changes
388
388
389 ? - display help
389 ? - display help
390
390
391 This command is not available when committing a merge.'''
391 This command is not available when committing a merge.'''
392
392
393 dorecord(ui, repo, commands.commit, *pats, **opts)
393 dorecord(ui, repo, commands.commit, *pats, **opts)
394
394
395
395
396 def qrecord(ui, repo, patch, *pats, **opts):
396 def qrecord(ui, repo, patch, *pats, **opts):
397 '''interactively record a new patch
397 '''interactively record a new patch
398
398
399 See :hg:`help qnew` & :hg:`help record` for more information and
399 See :hg:`help qnew` & :hg:`help record` for more information and
400 usage.
400 usage.
401 '''
401 '''
402
402
403 try:
403 try:
404 mq = extensions.find('mq')
404 mq = extensions.find('mq')
405 except KeyError:
405 except KeyError:
406 raise util.Abort(_("'mq' extension not loaded"))
406 raise util.Abort(_("'mq' extension not loaded"))
407
407
408 def committomq(ui, repo, *pats, **opts):
408 def committomq(ui, repo, *pats, **opts):
409 mq.new(ui, repo, patch, *pats, **opts)
409 mq.new(ui, repo, patch, *pats, **opts)
410
410
411 opts = opts.copy()
412 opts['force'] = True # always 'qnew -f'
413 dorecord(ui, repo, committomq, *pats, **opts)
411 dorecord(ui, repo, committomq, *pats, **opts)
414
412
415
413
416 def dorecord(ui, repo, commitfunc, *pats, **opts):
414 def dorecord(ui, repo, commitfunc, *pats, **opts):
417 if not ui.interactive():
415 if not ui.interactive():
418 raise util.Abort(_('running non-interactively, use commit instead'))
416 raise util.Abort(_('running non-interactively, use commit instead'))
419
417
420 def recordfunc(ui, repo, message, match, opts):
418 def recordfunc(ui, repo, message, match, opts):
421 """This is generic record driver.
419 """This is generic record driver.
422
420
423 Its job is to interactively filter local changes, and
421 Its job is to interactively filter local changes, and
424 accordingly prepare working directory into a state in which the
422 accordingly prepare working directory into a state in which the
425 job can be delegated to a non-interactive commit command such as
423 job can be delegated to a non-interactive commit command such as
426 'commit' or 'qrefresh'.
424 'commit' or 'qrefresh'.
427
425
428 After the actual job is done by non-interactive command, the
426 After the actual job is done by non-interactive command, the
429 working directory is restored to its original state.
427 working directory is restored to its original state.
430
428
431 In the end we'll record interesting changes, and everything else
429 In the end we'll record interesting changes, and everything else
432 will be left in place, so the user can continue working.
430 will be left in place, so the user can continue working.
433 """
431 """
434
432
435 merge = len(repo[None].parents()) > 1
433 merge = len(repo[None].parents()) > 1
436 if merge:
434 if merge:
437 raise util.Abort(_('cannot partially commit a merge '
435 raise util.Abort(_('cannot partially commit a merge '
438 '(use "hg commit" instead)'))
436 '(use "hg commit" instead)'))
439
437
440 changes = repo.status(match=match)[:3]
438 changes = repo.status(match=match)[:3]
441 diffopts = mdiff.diffopts(git=True, nodates=True)
439 diffopts = mdiff.diffopts(git=True, nodates=True)
442 chunks = patch.diff(repo, changes=changes, opts=diffopts)
440 chunks = patch.diff(repo, changes=changes, opts=diffopts)
443 fp = cStringIO.StringIO()
441 fp = cStringIO.StringIO()
444 fp.write(''.join(chunks))
442 fp.write(''.join(chunks))
445 fp.seek(0)
443 fp.seek(0)
446
444
447 # 1. filter patch, so we have intending-to apply subset of it
445 # 1. filter patch, so we have intending-to apply subset of it
448 chunks = filterpatch(ui, parsepatch(fp))
446 chunks = filterpatch(ui, parsepatch(fp))
449 del fp
447 del fp
450
448
451 contenders = set()
449 contenders = set()
452 for h in chunks:
450 for h in chunks:
453 try:
451 try:
454 contenders.update(set(h.files()))
452 contenders.update(set(h.files()))
455 except AttributeError:
453 except AttributeError:
456 pass
454 pass
457
455
458 changed = changes[0] + changes[1] + changes[2]
456 changed = changes[0] + changes[1] + changes[2]
459 newfiles = [f for f in changed if f in contenders]
457 newfiles = [f for f in changed if f in contenders]
460 if not newfiles:
458 if not newfiles:
461 ui.status(_('no changes to record\n'))
459 ui.status(_('no changes to record\n'))
462 return 0
460 return 0
463
461
464 modified = set(changes[0])
462 modified = set(changes[0])
465
463
466 # 2. backup changed files, so we can restore them in the end
464 # 2. backup changed files, so we can restore them in the end
467 backups = {}
465 backups = {}
468 backupdir = repo.join('record-backups')
466 backupdir = repo.join('record-backups')
469 try:
467 try:
470 os.mkdir(backupdir)
468 os.mkdir(backupdir)
471 except OSError, err:
469 except OSError, err:
472 if err.errno != errno.EEXIST:
470 if err.errno != errno.EEXIST:
473 raise
471 raise
474 try:
472 try:
475 # backup continues
473 # backup continues
476 for f in newfiles:
474 for f in newfiles:
477 if f not in modified:
475 if f not in modified:
478 continue
476 continue
479 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
477 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
480 dir=backupdir)
478 dir=backupdir)
481 os.close(fd)
479 os.close(fd)
482 ui.debug('backup %r as %r\n' % (f, tmpname))
480 ui.debug('backup %r as %r\n' % (f, tmpname))
483 util.copyfile(repo.wjoin(f), tmpname)
481 util.copyfile(repo.wjoin(f), tmpname)
484 shutil.copystat(repo.wjoin(f), tmpname)
482 shutil.copystat(repo.wjoin(f), tmpname)
485 backups[f] = tmpname
483 backups[f] = tmpname
486
484
487 fp = cStringIO.StringIO()
485 fp = cStringIO.StringIO()
488 for c in chunks:
486 for c in chunks:
489 if c.filename() in backups:
487 if c.filename() in backups:
490 c.write(fp)
488 c.write(fp)
491 dopatch = fp.tell()
489 dopatch = fp.tell()
492 fp.seek(0)
490 fp.seek(0)
493
491
494 # 3a. apply filtered patch to clean repo (clean)
492 # 3a. apply filtered patch to clean repo (clean)
495 if backups:
493 if backups:
496 hg.revert(repo, repo.dirstate.parents()[0],
494 hg.revert(repo, repo.dirstate.parents()[0],
497 lambda key: key in backups)
495 lambda key: key in backups)
498
496
499 # 3b. (apply)
497 # 3b. (apply)
500 if dopatch:
498 if dopatch:
501 try:
499 try:
502 ui.debug('applying patch\n')
500 ui.debug('applying patch\n')
503 ui.debug(fp.getvalue())
501 ui.debug(fp.getvalue())
504 pfiles = {}
502 pfiles = {}
505 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
503 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
506 eolmode=None)
504 eolmode=None)
507 cmdutil.updatedir(ui, repo, pfiles)
505 cmdutil.updatedir(ui, repo, pfiles)
508 except patch.PatchError, err:
506 except patch.PatchError, err:
509 raise util.Abort(str(err))
507 raise util.Abort(str(err))
510 del fp
508 del fp
511
509
512 # 4. We prepared working directory according to filtered
510 # 4. We prepared working directory according to filtered
513 # patch. Now is the time to delegate the job to
511 # patch. Now is the time to delegate the job to
514 # commit/qrefresh or the like!
512 # commit/qrefresh or the like!
515
513
516 # it is important to first chdir to repo root -- we'll call
514 # it is important to first chdir to repo root -- we'll call
517 # a highlevel command with list of pathnames relative to
515 # a highlevel command with list of pathnames relative to
518 # repo root
516 # repo root
519 cwd = os.getcwd()
517 cwd = os.getcwd()
520 os.chdir(repo.root)
518 os.chdir(repo.root)
521 try:
519 try:
522 commitfunc(ui, repo, *newfiles, **opts)
520 commitfunc(ui, repo, *newfiles, **opts)
523 finally:
521 finally:
524 os.chdir(cwd)
522 os.chdir(cwd)
525
523
526 return 0
524 return 0
527 finally:
525 finally:
528 # 5. finally restore backed-up files
526 # 5. finally restore backed-up files
529 try:
527 try:
530 for realname, tmpname in backups.iteritems():
528 for realname, tmpname in backups.iteritems():
531 ui.debug('restoring %r to %r\n' % (tmpname, realname))
529 ui.debug('restoring %r to %r\n' % (tmpname, realname))
532 util.copyfile(tmpname, repo.wjoin(realname))
530 util.copyfile(tmpname, repo.wjoin(realname))
533 # Our calls to copystat() here and above are a
531 # Our calls to copystat() here and above are a
534 # hack to trick any editors that have f open that
532 # hack to trick any editors that have f open that
535 # we haven't modified them.
533 # we haven't modified them.
536 #
534 #
537 # Also note that this racy as an editor could
535 # Also note that this racy as an editor could
538 # notice the file's mtime before we've finished
536 # notice the file's mtime before we've finished
539 # writing it.
537 # writing it.
540 shutil.copystat(tmpname, repo.wjoin(realname))
538 shutil.copystat(tmpname, repo.wjoin(realname))
541 os.unlink(tmpname)
539 os.unlink(tmpname)
542 os.rmdir(backupdir)
540 os.rmdir(backupdir)
543 except OSError:
541 except OSError:
544 pass
542 pass
545
543
546 # wrap ui.write so diff output can be labeled/colorized
544 # wrap ui.write so diff output can be labeled/colorized
547 def wrapwrite(orig, *args, **kw):
545 def wrapwrite(orig, *args, **kw):
548 label = kw.pop('label', '')
546 label = kw.pop('label', '')
549 for chunk, l in patch.difflabel(lambda: args):
547 for chunk, l in patch.difflabel(lambda: args):
550 orig(chunk, label=label + l)
548 orig(chunk, label=label + l)
551 oldwrite = ui.write
549 oldwrite = ui.write
552 extensions.wrapfunction(ui, 'write', wrapwrite)
550 extensions.wrapfunction(ui, 'write', wrapwrite)
553 try:
551 try:
554 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
552 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
555 finally:
553 finally:
556 ui.write = oldwrite
554 ui.write = oldwrite
557
555
558 cmdtable = {
556 cmdtable = {
559 "record":
557 "record":
560 (record,
558 (record, commands.table['^commit|ci'][1], # same options as commit
561
562 # add commit options
563 commands.table['^commit|ci'][1],
564
565 _('hg record [OPTION]... [FILE]...')),
559 _('hg record [OPTION]... [FILE]...')),
566 }
560 }
567
561
568
562
569 def uisetup(ui):
563 def uisetup(ui):
570 try:
564 try:
571 mq = extensions.find('mq')
565 mq = extensions.find('mq')
572 except KeyError:
566 except KeyError:
573 return
567 return
574
568
575 qcmdtable = {
569 qcmdtable = {
576 "qrecord":
570 "qrecord":
577 (qrecord,
571 (qrecord, mq.cmdtable['^qnew'][1], # same options as qnew
578
579 # add qnew options, except '--force'
580 [opt for opt in mq.cmdtable['^qnew'][1] if opt[1] != 'force'],
581
582 _('hg qrecord [OPTION]... PATCH [FILE]...')),
572 _('hg qrecord [OPTION]... PATCH [FILE]...')),
583 }
573 }
584
574
585 cmdtable.update(qcmdtable)
575 cmdtable.update(qcmdtable)
586
576
General Comments 0
You need to be logged in to leave comments. Login now