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