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