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