##// END OF EJS Templates
record: turn consumefile() into a pure function
Patrick Mezard -
r13290:82133e91 default
parent child Browse files
Show More
@@ -1,576 +1,575
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 def consumefile(chunks):
259 chunks.reverse()
260 seen = set()
261 def consumefile():
262 """fetch next portion from chunks until a 'header' is seen
259 """fetch next portion from chunks until a 'header' is seen
263 NB: header == new-file mark
260 NB: header == new-file mark
264 """
261 """
265 consumed = []
262 consumed = []
266 while chunks:
263 while chunks:
267 if isinstance(chunks[-1], header):
264 if isinstance(chunks[-1], header):
268 break
265 break
269 else:
266 consumed.append(chunks.pop())
270 consumed.append(chunks.pop())
271 return consumed
267 return consumed
272
268
269 chunks = list(chunks)
270 chunks.reverse()
271 seen = set()
273 resp_all = [None] # this two are changed from inside prompt,
272 resp_all = [None] # this two are changed from inside prompt,
274 resp_file = [None] # so can't be usual variables
273 resp_file = [None] # so can't be usual variables
275 applied = {} # 'filename' -> [] of chunks
274 applied = {} # 'filename' -> [] of chunks
276 def prompt(query):
275 def prompt(query):
277 """prompt query, and process base inputs
276 """prompt query, and process base inputs
278
277
279 - y/n for the rest of file
278 - y/n for the rest of file
280 - y/n for the rest
279 - y/n for the rest
281 - ? (help)
280 - ? (help)
282 - q (quit)
281 - q (quit)
283
282
284 Returns True/False and sets reps_all and resp_file as
283 Returns True/False and sets reps_all and resp_file as
285 appropriate.
284 appropriate.
286 """
285 """
287 if resp_all[0] is not None:
286 if resp_all[0] is not None:
288 return resp_all[0]
287 return resp_all[0]
289 if resp_file[0] is not None:
288 if resp_file[0] is not None:
290 return resp_file[0]
289 return resp_file[0]
291 while True:
290 while True:
292 resps = _('[Ynsfdaq?]')
291 resps = _('[Ynsfdaq?]')
293 choices = (_('&Yes, record this change'),
292 choices = (_('&Yes, record this change'),
294 _('&No, skip this change'),
293 _('&No, skip this change'),
295 _('&Skip remaining changes to this file'),
294 _('&Skip remaining changes to this file'),
296 _('Record remaining changes to this &file'),
295 _('Record remaining changes to this &file'),
297 _('&Done, skip remaining changes and files'),
296 _('&Done, skip remaining changes and files'),
298 _('Record &all changes to all remaining files'),
297 _('Record &all changes to all remaining files'),
299 _('&Quit, recording no changes'),
298 _('&Quit, recording no changes'),
300 _('&?'))
299 _('&?'))
301 r = ui.promptchoice("%s %s" % (query, resps), choices)
300 r = ui.promptchoice("%s %s" % (query, resps), choices)
302 ui.write("\n")
301 ui.write("\n")
303 if r == 7: # ?
302 if r == 7: # ?
304 doc = gettext(record.__doc__)
303 doc = gettext(record.__doc__)
305 c = doc.find('::') + 2
304 c = doc.find('::') + 2
306 for l in doc[c:].splitlines():
305 for l in doc[c:].splitlines():
307 if l.startswith(' '):
306 if l.startswith(' '):
308 ui.write(l.strip(), '\n')
307 ui.write(l.strip(), '\n')
309 continue
308 continue
310 elif r == 0: # yes
309 elif r == 0: # yes
311 ret = True
310 ret = True
312 elif r == 1: # no
311 elif r == 1: # no
313 ret = False
312 ret = False
314 elif r == 2: # Skip
313 elif r == 2: # Skip
315 ret = resp_file[0] = False
314 ret = resp_file[0] = False
316 elif r == 3: # file (Record remaining)
315 elif r == 3: # file (Record remaining)
317 ret = resp_file[0] = True
316 ret = resp_file[0] = True
318 elif r == 4: # done, skip remaining
317 elif r == 4: # done, skip remaining
319 ret = resp_all[0] = False
318 ret = resp_all[0] = False
320 elif r == 5: # all
319 elif r == 5: # all
321 ret = resp_all[0] = True
320 ret = resp_all[0] = True
322 elif r == 6: # quit
321 elif r == 6: # quit
323 raise util.Abort(_('user quit'))
322 raise util.Abort(_('user quit'))
324 return ret
323 return ret
325 pos, total = 0, len(chunks) - 1
324 pos, total = 0, len(chunks) - 1
326 while chunks:
325 while chunks:
327 pos = total - len(chunks) + 1
326 pos = total - len(chunks) + 1
328 chunk = chunks.pop()
327 chunk = chunks.pop()
329 if isinstance(chunk, header):
328 if isinstance(chunk, header):
330 # new-file mark
329 # new-file mark
331 resp_file = [None]
330 resp_file = [None]
332 fixoffset = 0
331 fixoffset = 0
333 hdr = ''.join(chunk.header)
332 hdr = ''.join(chunk.header)
334 if hdr in seen:
333 if hdr in seen:
335 consumefile()
334 consumefile(chunks)
336 continue
335 continue
337 seen.add(hdr)
336 seen.add(hdr)
338 if resp_all[0] is None:
337 if resp_all[0] is None:
339 chunk.pretty(ui)
338 chunk.pretty(ui)
340 r = prompt(_('examine changes to %s?') %
339 r = prompt(_('examine changes to %s?') %
341 _(' and ').join(map(repr, chunk.files())))
340 _(' and ').join(map(repr, chunk.files())))
342 if r:
341 if r:
343 applied[chunk.filename()] = [chunk]
342 applied[chunk.filename()] = [chunk]
344 if chunk.allhunks():
343 if chunk.allhunks():
345 applied[chunk.filename()] += consumefile()
344 applied[chunk.filename()] += consumefile(chunks)
346 else:
345 else:
347 consumefile()
346 consumefile(chunks)
348 else:
347 else:
349 # new hunk
348 # new hunk
350 if resp_file[0] is None and resp_all[0] is None:
349 if resp_file[0] is None and resp_all[0] is None:
351 chunk.pretty(ui)
350 chunk.pretty(ui)
352 r = (total == 1
351 r = (total == 1
353 and prompt(_('record this change to %r?') % chunk.filename())
352 and prompt(_('record this change to %r?') % chunk.filename())
354 or prompt(_('record change %d/%d to %r?') %
353 or prompt(_('record change %d/%d to %r?') %
355 (pos, total, chunk.filename())))
354 (pos, total, chunk.filename())))
356 if r:
355 if r:
357 if fixoffset:
356 if fixoffset:
358 chunk = copy.copy(chunk)
357 chunk = copy.copy(chunk)
359 chunk.toline += fixoffset
358 chunk.toline += fixoffset
360 applied[chunk.filename()].append(chunk)
359 applied[chunk.filename()].append(chunk)
361 else:
360 else:
362 fixoffset += chunk.removed - chunk.added
361 fixoffset += chunk.removed - chunk.added
363 return sum([h for h in applied.itervalues()
362 return sum([h for h in applied.itervalues()
364 if h[0].special() or len(h) > 1], [])
363 if h[0].special() or len(h) > 1], [])
365
364
366 def record(ui, repo, *pats, **opts):
365 def record(ui, repo, *pats, **opts):
367 '''interactively select changes to commit
366 '''interactively select changes to commit
368
367
369 If a list of files is omitted, all changes reported by :hg:`status`
368 If a list of files is omitted, all changes reported by :hg:`status`
370 will be candidates for recording.
369 will be candidates for recording.
371
370
372 See :hg:`help dates` for a list of formats valid for -d/--date.
371 See :hg:`help dates` for a list of formats valid for -d/--date.
373
372
374 You will be prompted for whether to record changes to each
373 You will be prompted for whether to record changes to each
375 modified file, and for files with multiple changes, for each
374 modified file, and for files with multiple changes, for each
376 change to use. For each query, the following responses are
375 change to use. For each query, the following responses are
377 possible::
376 possible::
378
377
379 y - record this change
378 y - record this change
380 n - skip this change
379 n - skip this change
381
380
382 s - skip remaining changes to this file
381 s - skip remaining changes to this file
383 f - record remaining changes to this file
382 f - record remaining changes to this file
384
383
385 d - done, skip remaining changes and files
384 d - done, skip remaining changes and files
386 a - record all changes to all remaining files
385 a - record all changes to all remaining files
387 q - quit, recording no changes
386 q - quit, recording no changes
388
387
389 ? - display help
388 ? - display help
390
389
391 This command is not available when committing a merge.'''
390 This command is not available when committing a merge.'''
392
391
393 dorecord(ui, repo, commands.commit, *pats, **opts)
392 dorecord(ui, repo, commands.commit, *pats, **opts)
394
393
395
394
396 def qrecord(ui, repo, patch, *pats, **opts):
395 def qrecord(ui, repo, patch, *pats, **opts):
397 '''interactively record a new patch
396 '''interactively record a new patch
398
397
399 See :hg:`help qnew` & :hg:`help record` for more information and
398 See :hg:`help qnew` & :hg:`help record` for more information and
400 usage.
399 usage.
401 '''
400 '''
402
401
403 try:
402 try:
404 mq = extensions.find('mq')
403 mq = extensions.find('mq')
405 except KeyError:
404 except KeyError:
406 raise util.Abort(_("'mq' extension not loaded"))
405 raise util.Abort(_("'mq' extension not loaded"))
407
406
408 def committomq(ui, repo, *pats, **opts):
407 def committomq(ui, repo, *pats, **opts):
409 mq.new(ui, repo, patch, *pats, **opts)
408 mq.new(ui, repo, patch, *pats, **opts)
410
409
411 dorecord(ui, repo, committomq, *pats, **opts)
410 dorecord(ui, repo, committomq, *pats, **opts)
412
411
413
412
414 def dorecord(ui, repo, commitfunc, *pats, **opts):
413 def dorecord(ui, repo, commitfunc, *pats, **opts):
415 if not ui.interactive():
414 if not ui.interactive():
416 raise util.Abort(_('running non-interactively, use commit instead'))
415 raise util.Abort(_('running non-interactively, use commit instead'))
417
416
418 def recordfunc(ui, repo, message, match, opts):
417 def recordfunc(ui, repo, message, match, opts):
419 """This is generic record driver.
418 """This is generic record driver.
420
419
421 Its job is to interactively filter local changes, and
420 Its job is to interactively filter local changes, and
422 accordingly prepare working directory into a state in which the
421 accordingly prepare working directory into a state in which the
423 job can be delegated to a non-interactive commit command such as
422 job can be delegated to a non-interactive commit command such as
424 'commit' or 'qrefresh'.
423 'commit' or 'qrefresh'.
425
424
426 After the actual job is done by non-interactive command, the
425 After the actual job is done by non-interactive command, the
427 working directory is restored to its original state.
426 working directory is restored to its original state.
428
427
429 In the end we'll record interesting changes, and everything else
428 In the end we'll record interesting changes, and everything else
430 will be left in place, so the user can continue working.
429 will be left in place, so the user can continue working.
431 """
430 """
432
431
433 merge = len(repo[None].parents()) > 1
432 merge = len(repo[None].parents()) > 1
434 if merge:
433 if merge:
435 raise util.Abort(_('cannot partially commit a merge '
434 raise util.Abort(_('cannot partially commit a merge '
436 '(use "hg commit" instead)'))
435 '(use "hg commit" instead)'))
437
436
438 changes = repo.status(match=match)[:3]
437 changes = repo.status(match=match)[:3]
439 diffopts = mdiff.diffopts(git=True, nodates=True)
438 diffopts = mdiff.diffopts(git=True, nodates=True)
440 chunks = patch.diff(repo, changes=changes, opts=diffopts)
439 chunks = patch.diff(repo, changes=changes, opts=diffopts)
441 fp = cStringIO.StringIO()
440 fp = cStringIO.StringIO()
442 fp.write(''.join(chunks))
441 fp.write(''.join(chunks))
443 fp.seek(0)
442 fp.seek(0)
444
443
445 # 1. filter patch, so we have intending-to apply subset of it
444 # 1. filter patch, so we have intending-to apply subset of it
446 chunks = filterpatch(ui, parsepatch(fp))
445 chunks = filterpatch(ui, parsepatch(fp))
447 del fp
446 del fp
448
447
449 contenders = set()
448 contenders = set()
450 for h in chunks:
449 for h in chunks:
451 try:
450 try:
452 contenders.update(set(h.files()))
451 contenders.update(set(h.files()))
453 except AttributeError:
452 except AttributeError:
454 pass
453 pass
455
454
456 changed = changes[0] + changes[1] + changes[2]
455 changed = changes[0] + changes[1] + changes[2]
457 newfiles = [f for f in changed if f in contenders]
456 newfiles = [f for f in changed if f in contenders]
458 if not newfiles:
457 if not newfiles:
459 ui.status(_('no changes to record\n'))
458 ui.status(_('no changes to record\n'))
460 return 0
459 return 0
461
460
462 modified = set(changes[0])
461 modified = set(changes[0])
463
462
464 # 2. backup changed files, so we can restore them in the end
463 # 2. backup changed files, so we can restore them in the end
465 backups = {}
464 backups = {}
466 backupdir = repo.join('record-backups')
465 backupdir = repo.join('record-backups')
467 try:
466 try:
468 os.mkdir(backupdir)
467 os.mkdir(backupdir)
469 except OSError, err:
468 except OSError, err:
470 if err.errno != errno.EEXIST:
469 if err.errno != errno.EEXIST:
471 raise
470 raise
472 try:
471 try:
473 # backup continues
472 # backup continues
474 for f in newfiles:
473 for f in newfiles:
475 if f not in modified:
474 if f not in modified:
476 continue
475 continue
477 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
476 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
478 dir=backupdir)
477 dir=backupdir)
479 os.close(fd)
478 os.close(fd)
480 ui.debug('backup %r as %r\n' % (f, tmpname))
479 ui.debug('backup %r as %r\n' % (f, tmpname))
481 util.copyfile(repo.wjoin(f), tmpname)
480 util.copyfile(repo.wjoin(f), tmpname)
482 shutil.copystat(repo.wjoin(f), tmpname)
481 shutil.copystat(repo.wjoin(f), tmpname)
483 backups[f] = tmpname
482 backups[f] = tmpname
484
483
485 fp = cStringIO.StringIO()
484 fp = cStringIO.StringIO()
486 for c in chunks:
485 for c in chunks:
487 if c.filename() in backups:
486 if c.filename() in backups:
488 c.write(fp)
487 c.write(fp)
489 dopatch = fp.tell()
488 dopatch = fp.tell()
490 fp.seek(0)
489 fp.seek(0)
491
490
492 # 3a. apply filtered patch to clean repo (clean)
491 # 3a. apply filtered patch to clean repo (clean)
493 if backups:
492 if backups:
494 hg.revert(repo, repo.dirstate.parents()[0],
493 hg.revert(repo, repo.dirstate.parents()[0],
495 lambda key: key in backups)
494 lambda key: key in backups)
496
495
497 # 3b. (apply)
496 # 3b. (apply)
498 if dopatch:
497 if dopatch:
499 try:
498 try:
500 ui.debug('applying patch\n')
499 ui.debug('applying patch\n')
501 ui.debug(fp.getvalue())
500 ui.debug(fp.getvalue())
502 pfiles = {}
501 pfiles = {}
503 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
502 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
504 eolmode=None)
503 eolmode=None)
505 cmdutil.updatedir(ui, repo, pfiles)
504 cmdutil.updatedir(ui, repo, pfiles)
506 except patch.PatchError, err:
505 except patch.PatchError, err:
507 raise util.Abort(str(err))
506 raise util.Abort(str(err))
508 del fp
507 del fp
509
508
510 # 4. We prepared working directory according to filtered
509 # 4. We prepared working directory according to filtered
511 # patch. Now is the time to delegate the job to
510 # patch. Now is the time to delegate the job to
512 # commit/qrefresh or the like!
511 # commit/qrefresh or the like!
513
512
514 # it is important to first chdir to repo root -- we'll call
513 # it is important to first chdir to repo root -- we'll call
515 # a highlevel command with list of pathnames relative to
514 # a highlevel command with list of pathnames relative to
516 # repo root
515 # repo root
517 cwd = os.getcwd()
516 cwd = os.getcwd()
518 os.chdir(repo.root)
517 os.chdir(repo.root)
519 try:
518 try:
520 commitfunc(ui, repo, *newfiles, **opts)
519 commitfunc(ui, repo, *newfiles, **opts)
521 finally:
520 finally:
522 os.chdir(cwd)
521 os.chdir(cwd)
523
522
524 return 0
523 return 0
525 finally:
524 finally:
526 # 5. finally restore backed-up files
525 # 5. finally restore backed-up files
527 try:
526 try:
528 for realname, tmpname in backups.iteritems():
527 for realname, tmpname in backups.iteritems():
529 ui.debug('restoring %r to %r\n' % (tmpname, realname))
528 ui.debug('restoring %r to %r\n' % (tmpname, realname))
530 util.copyfile(tmpname, repo.wjoin(realname))
529 util.copyfile(tmpname, repo.wjoin(realname))
531 # Our calls to copystat() here and above are a
530 # Our calls to copystat() here and above are a
532 # hack to trick any editors that have f open that
531 # hack to trick any editors that have f open that
533 # we haven't modified them.
532 # we haven't modified them.
534 #
533 #
535 # Also note that this racy as an editor could
534 # Also note that this racy as an editor could
536 # notice the file's mtime before we've finished
535 # notice the file's mtime before we've finished
537 # writing it.
536 # writing it.
538 shutil.copystat(tmpname, repo.wjoin(realname))
537 shutil.copystat(tmpname, repo.wjoin(realname))
539 os.unlink(tmpname)
538 os.unlink(tmpname)
540 os.rmdir(backupdir)
539 os.rmdir(backupdir)
541 except OSError:
540 except OSError:
542 pass
541 pass
543
542
544 # wrap ui.write so diff output can be labeled/colorized
543 # wrap ui.write so diff output can be labeled/colorized
545 def wrapwrite(orig, *args, **kw):
544 def wrapwrite(orig, *args, **kw):
546 label = kw.pop('label', '')
545 label = kw.pop('label', '')
547 for chunk, l in patch.difflabel(lambda: args):
546 for chunk, l in patch.difflabel(lambda: args):
548 orig(chunk, label=label + l)
547 orig(chunk, label=label + l)
549 oldwrite = ui.write
548 oldwrite = ui.write
550 extensions.wrapfunction(ui, 'write', wrapwrite)
549 extensions.wrapfunction(ui, 'write', wrapwrite)
551 try:
550 try:
552 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
551 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
553 finally:
552 finally:
554 ui.write = oldwrite
553 ui.write = oldwrite
555
554
556 cmdtable = {
555 cmdtable = {
557 "record":
556 "record":
558 (record, commands.table['^commit|ci'][1], # same options as commit
557 (record, commands.table['^commit|ci'][1], # same options as commit
559 _('hg record [OPTION]... [FILE]...')),
558 _('hg record [OPTION]... [FILE]...')),
560 }
559 }
561
560
562
561
563 def uisetup(ui):
562 def uisetup(ui):
564 try:
563 try:
565 mq = extensions.find('mq')
564 mq = extensions.find('mq')
566 except KeyError:
565 except KeyError:
567 return
566 return
568
567
569 qcmdtable = {
568 qcmdtable = {
570 "qrecord":
569 "qrecord":
571 (qrecord, mq.cmdtable['^qnew'][1], # same options as qnew
570 (qrecord, mq.cmdtable['^qnew'][1], # same options as qnew
572 _('hg qrecord [OPTION]... PATCH [FILE]...')),
571 _('hg qrecord [OPTION]... PATCH [FILE]...')),
573 }
572 }
574
573
575 cmdtable.update(qcmdtable)
574 cmdtable.update(qcmdtable)
576
575
General Comments 0
You need to be logged in to leave comments. Login now