##// END OF EJS Templates
record: minimize number of status calls
Dirkjan Ochtman -
r7754:ab00d2c2 default
parent child Browse files
Show More
@@ -1,542 +1,534 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
5 # This software may be used and distributed according to the terms of
6 # the GNU General Public License, incorporated herein by reference.
6 # the GNU General Public License, 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 = {}
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 choices = _('[Ynsfdaq?]')
285 choices = _('[Ynsfdaq?]')
286 r = (ui.prompt("%s %s " % (query, choices), '(?i)%s?$' % choices)
286 r = (ui.prompt("%s %s " % (query, choices), '(?i)%s?$' % choices)
287 or _('y')).lower()
287 or _('y')).lower()
288 if r == _('?'):
288 if r == _('?'):
289 doc = gettext(record.__doc__)
289 doc = gettext(record.__doc__)
290 c = doc.find(_('y - record this change'))
290 c = doc.find(_('y - record this change'))
291 for l in doc[c:].splitlines():
291 for l in doc[c:].splitlines():
292 if l: ui.write(l.strip(), '\n')
292 if l: ui.write(l.strip(), '\n')
293 continue
293 continue
294 elif r == _('s'):
294 elif r == _('s'):
295 r = resp_file[0] = 'n'
295 r = resp_file[0] = 'n'
296 elif r == _('f'):
296 elif r == _('f'):
297 r = resp_file[0] = 'y'
297 r = resp_file[0] = 'y'
298 elif r == _('d'):
298 elif r == _('d'):
299 r = resp_all[0] = 'n'
299 r = resp_all[0] = 'n'
300 elif r == _('a'):
300 elif r == _('a'):
301 r = resp_all[0] = 'y'
301 r = resp_all[0] = 'y'
302 elif r == _('q'):
302 elif r == _('q'):
303 raise util.Abort(_('user quit'))
303 raise util.Abort(_('user quit'))
304 return r
304 return r
305 while chunks:
305 while chunks:
306 chunk = chunks.pop()
306 chunk = chunks.pop()
307 if isinstance(chunk, header):
307 if isinstance(chunk, header):
308 # new-file mark
308 # new-file mark
309 resp_file = [None]
309 resp_file = [None]
310 fixoffset = 0
310 fixoffset = 0
311 hdr = ''.join(chunk.header)
311 hdr = ''.join(chunk.header)
312 if hdr in seen:
312 if hdr in seen:
313 consumefile()
313 consumefile()
314 continue
314 continue
315 seen[hdr] = True
315 seen[hdr] = True
316 if resp_all[0] is None:
316 if resp_all[0] is None:
317 chunk.pretty(ui)
317 chunk.pretty(ui)
318 r = prompt(_('examine changes to %s?') %
318 r = prompt(_('examine changes to %s?') %
319 _(' and ').join(map(repr, chunk.files())))
319 _(' and ').join(map(repr, chunk.files())))
320 if r == _('y'):
320 if r == _('y'):
321 applied[chunk.filename()] = [chunk]
321 applied[chunk.filename()] = [chunk]
322 if chunk.allhunks():
322 if chunk.allhunks():
323 applied[chunk.filename()] += consumefile()
323 applied[chunk.filename()] += consumefile()
324 else:
324 else:
325 consumefile()
325 consumefile()
326 else:
326 else:
327 # new hunk
327 # new hunk
328 if resp_file[0] is None and resp_all[0] is None:
328 if resp_file[0] is None and resp_all[0] is None:
329 chunk.pretty(ui)
329 chunk.pretty(ui)
330 r = prompt(_('record this change to %r?') %
330 r = prompt(_('record this change to %r?') %
331 chunk.filename())
331 chunk.filename())
332 if r == _('y'):
332 if r == _('y'):
333 if fixoffset:
333 if fixoffset:
334 chunk = copy.copy(chunk)
334 chunk = copy.copy(chunk)
335 chunk.toline += fixoffset
335 chunk.toline += fixoffset
336 applied[chunk.filename()].append(chunk)
336 applied[chunk.filename()].append(chunk)
337 else:
337 else:
338 fixoffset += chunk.removed - chunk.added
338 fixoffset += chunk.removed - chunk.added
339 return reduce(operator.add, [h for h in applied.itervalues()
339 return reduce(operator.add, [h for h in applied.itervalues()
340 if h[0].special() or len(h) > 1], [])
340 if h[0].special() or len(h) > 1], [])
341
341
342 def record(ui, repo, *pats, **opts):
342 def record(ui, repo, *pats, **opts):
343 '''interactively select changes to commit
343 '''interactively select changes to commit
344
344
345 If a list of files is omitted, all changes reported by "hg status"
345 If a list of files is omitted, all changes reported by "hg status"
346 will be candidates for recording.
346 will be candidates for recording.
347
347
348 See 'hg help dates' for a list of formats valid for -d/--date.
348 See 'hg help dates' for a list of formats valid for -d/--date.
349
349
350 You will be prompted for whether to record changes to each
350 You will be prompted for whether to record changes to each
351 modified file, and for files with multiple changes, for each
351 modified file, and for files with multiple changes, for each
352 change to use. For each query, the following responses are
352 change to use. For each query, the following responses are
353 possible:
353 possible:
354
354
355 y - record this change
355 y - record this change
356 n - skip this change
356 n - skip this change
357
357
358 s - skip remaining changes to this file
358 s - skip remaining changes to this file
359 f - record remaining changes to this file
359 f - record remaining changes to this file
360
360
361 d - done, skip remaining changes and files
361 d - done, skip remaining changes and files
362 a - record all changes to all remaining files
362 a - record all changes to all remaining files
363 q - quit, recording no changes
363 q - quit, recording no changes
364
364
365 ? - display help'''
365 ? - display help'''
366
366
367 def record_committer(ui, repo, pats, opts):
367 def record_committer(ui, repo, pats, opts):
368 commands.commit(ui, repo, *pats, **opts)
368 commands.commit(ui, repo, *pats, **opts)
369
369
370 dorecord(ui, repo, record_committer, *pats, **opts)
370 dorecord(ui, repo, record_committer, *pats, **opts)
371
371
372
372
373 def qrecord(ui, repo, patch, *pats, **opts):
373 def qrecord(ui, repo, patch, *pats, **opts):
374 '''interactively record a new patch
374 '''interactively record a new patch
375
375
376 see 'hg help qnew' & 'hg help record' for more information and usage
376 see 'hg help qnew' & 'hg help record' for more information and usage
377 '''
377 '''
378
378
379 try:
379 try:
380 mq = extensions.find('mq')
380 mq = extensions.find('mq')
381 except KeyError:
381 except KeyError:
382 raise util.Abort(_("'mq' extension not loaded"))
382 raise util.Abort(_("'mq' extension not loaded"))
383
383
384 def qrecord_committer(ui, repo, pats, opts):
384 def qrecord_committer(ui, repo, pats, opts):
385 mq.new(ui, repo, patch, *pats, **opts)
385 mq.new(ui, repo, patch, *pats, **opts)
386
386
387 opts = opts.copy()
387 opts = opts.copy()
388 opts['force'] = True # always 'qnew -f'
388 opts['force'] = True # always 'qnew -f'
389 dorecord(ui, repo, qrecord_committer, *pats, **opts)
389 dorecord(ui, repo, qrecord_committer, *pats, **opts)
390
390
391
391
392 def dorecord(ui, repo, committer, *pats, **opts):
392 def dorecord(ui, repo, committer, *pats, **opts):
393 if not ui.interactive:
393 if not ui.interactive:
394 raise util.Abort(_('running non-interactively, use commit instead'))
394 raise util.Abort(_('running non-interactively, use commit instead'))
395
395
396 def recordfunc(ui, repo, message, match, opts):
396 def recordfunc(ui, repo, message, match, opts):
397 """This is generic record driver.
397 """This is generic record driver.
398
398
399 It's job is to interactively filter local changes, and accordingly
399 It's job is to interactively filter local changes, and accordingly
400 prepare working dir into a state, where the job can be delegated to
400 prepare working dir into a state, where the job can be delegated to
401 non-interactive commit command such as 'commit' or 'qrefresh'.
401 non-interactive commit command such as 'commit' or 'qrefresh'.
402
402
403 After the actual job is done by non-interactive command, working dir
403 After the actual job is done by non-interactive command, working dir
404 state is restored to original.
404 state is restored to original.
405
405
406 In the end we'll record intresting changes, and everything else will be
406 In the end we'll record intresting changes, and everything else will be
407 left in place, so the user can continue his work.
407 left in place, so the user can continue his work.
408 """
408 """
409 if match.files():
409
410 changes = None
410 changes = repo.status(match=match)[:3]
411 else:
412 changes = repo.status(match=match)[:3]
413 modified, added, removed = changes
414 match = cmdutil.matchfiles(repo, modified + added + removed)
415 diffopts = mdiff.diffopts(git=True, nodates=True)
411 diffopts = mdiff.diffopts(git=True, nodates=True)
416 chunks = patch.diff(repo, repo.dirstate.parents()[0], match=match,
412 chunks = patch.diff(repo, changes=changes, opts=diffopts)
417 changes=changes, opts=diffopts)
418 fp = cStringIO.StringIO()
413 fp = cStringIO.StringIO()
419 fp.write(''.join(chunks))
414 fp.write(''.join(chunks))
420 fp.seek(0)
415 fp.seek(0)
421
416
422 # 1. filter patch, so we have intending-to apply subset of it
417 # 1. filter patch, so we have intending-to apply subset of it
423 chunks = filterpatch(ui, parsepatch(fp))
418 chunks = filterpatch(ui, parsepatch(fp))
424 del fp
419 del fp
425
420
426 contenders = {}
421 contenders = {}
427 for h in chunks:
422 for h in chunks:
428 try: contenders.update(dict.fromkeys(h.files()))
423 try: contenders.update(dict.fromkeys(h.files()))
429 except AttributeError: pass
424 except AttributeError: pass
430
425
431 newfiles = [f for f in match.files() if f in contenders]
426 changed = changes[0] + changes[1] + changes[2]
432
427 newfiles = [f for f in changed if f in contenders]
433 if not newfiles:
428 if not newfiles:
434 ui.status(_('no changes to record\n'))
429 ui.status(_('no changes to record\n'))
435 return 0
430 return 0
436
431
437 if changes is None:
438 match = cmdutil.matchfiles(repo, newfiles)
439 changes = repo.status(match=match)
440 modified = dict.fromkeys(changes[0])
432 modified = dict.fromkeys(changes[0])
441
433
442 # 2. backup changed files, so we can restore them in the end
434 # 2. backup changed files, so we can restore them in the end
443 backups = {}
435 backups = {}
444 backupdir = repo.join('record-backups')
436 backupdir = repo.join('record-backups')
445 try:
437 try:
446 os.mkdir(backupdir)
438 os.mkdir(backupdir)
447 except OSError, err:
439 except OSError, err:
448 if err.errno != errno.EEXIST:
440 if err.errno != errno.EEXIST:
449 raise
441 raise
450 try:
442 try:
451 # backup continues
443 # backup continues
452 for f in newfiles:
444 for f in newfiles:
453 if f not in modified:
445 if f not in modified:
454 continue
446 continue
455 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
447 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
456 dir=backupdir)
448 dir=backupdir)
457 os.close(fd)
449 os.close(fd)
458 ui.debug(_('backup %r as %r\n') % (f, tmpname))
450 ui.debug(_('backup %r as %r\n') % (f, tmpname))
459 util.copyfile(repo.wjoin(f), tmpname)
451 util.copyfile(repo.wjoin(f), tmpname)
460 backups[f] = tmpname
452 backups[f] = tmpname
461
453
462 fp = cStringIO.StringIO()
454 fp = cStringIO.StringIO()
463 for c in chunks:
455 for c in chunks:
464 if c.filename() in backups:
456 if c.filename() in backups:
465 c.write(fp)
457 c.write(fp)
466 dopatch = fp.tell()
458 dopatch = fp.tell()
467 fp.seek(0)
459 fp.seek(0)
468
460
469 # 3a. apply filtered patch to clean repo (clean)
461 # 3a. apply filtered patch to clean repo (clean)
470 if backups:
462 if backups:
471 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
463 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
472
464
473 # 3b. (apply)
465 # 3b. (apply)
474 if dopatch:
466 if dopatch:
475 try:
467 try:
476 ui.debug(_('applying patch\n'))
468 ui.debug(_('applying patch\n'))
477 ui.debug(fp.getvalue())
469 ui.debug(fp.getvalue())
478 pfiles = {}
470 pfiles = {}
479 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles)
471 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles)
480 patch.updatedir(ui, repo, pfiles)
472 patch.updatedir(ui, repo, pfiles)
481 except patch.PatchError, err:
473 except patch.PatchError, err:
482 s = str(err)
474 s = str(err)
483 if s:
475 if s:
484 raise util.Abort(s)
476 raise util.Abort(s)
485 else:
477 else:
486 raise util.Abort(_('patch failed to apply'))
478 raise util.Abort(_('patch failed to apply'))
487 del fp
479 del fp
488
480
489 # 4. We prepared working directory according to filtered patch.
481 # 4. We prepared working directory according to filtered patch.
490 # Now is the time to delegate the job to commit/qrefresh or the like!
482 # Now is the time to delegate the job to commit/qrefresh or the like!
491
483
492 # it is important to first chdir to repo root -- we'll call a
484 # it is important to first chdir to repo root -- we'll call a
493 # highlevel command with list of pathnames relative to repo root
485 # highlevel command with list of pathnames relative to repo root
494 cwd = os.getcwd()
486 cwd = os.getcwd()
495 os.chdir(repo.root)
487 os.chdir(repo.root)
496 try:
488 try:
497 committer(ui, repo, newfiles, opts)
489 committer(ui, repo, newfiles, opts)
498 finally:
490 finally:
499 os.chdir(cwd)
491 os.chdir(cwd)
500
492
501 return 0
493 return 0
502 finally:
494 finally:
503 # 5. finally restore backed-up files
495 # 5. finally restore backed-up files
504 try:
496 try:
505 for realname, tmpname in backups.iteritems():
497 for realname, tmpname in backups.iteritems():
506 ui.debug(_('restoring %r to %r\n') % (tmpname, realname))
498 ui.debug(_('restoring %r to %r\n') % (tmpname, realname))
507 util.copyfile(tmpname, repo.wjoin(realname))
499 util.copyfile(tmpname, repo.wjoin(realname))
508 os.unlink(tmpname)
500 os.unlink(tmpname)
509 os.rmdir(backupdir)
501 os.rmdir(backupdir)
510 except OSError:
502 except OSError:
511 pass
503 pass
512 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
504 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
513
505
514 cmdtable = {
506 cmdtable = {
515 "record":
507 "record":
516 (record,
508 (record,
517
509
518 # add commit options
510 # add commit options
519 commands.table['^commit|ci'][1],
511 commands.table['^commit|ci'][1],
520
512
521 _('hg record [OPTION]... [FILE]...')),
513 _('hg record [OPTION]... [FILE]...')),
522 }
514 }
523
515
524
516
525 def extsetup():
517 def extsetup():
526 try:
518 try:
527 mq = extensions.find('mq')
519 mq = extensions.find('mq')
528 except KeyError:
520 except KeyError:
529 return
521 return
530
522
531 qcmdtable = {
523 qcmdtable = {
532 "qrecord":
524 "qrecord":
533 (qrecord,
525 (qrecord,
534
526
535 # add qnew options, except '--force'
527 # add qnew options, except '--force'
536 [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'],
528 [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'],
537
529
538 _('hg qrecord [OPTION]... PATCH [FILE]...')),
530 _('hg qrecord [OPTION]... PATCH [FILE]...')),
539 }
531 }
540
532
541 cmdtable.update(qcmdtable)
533 cmdtable.update(qcmdtable)
542
534
General Comments 0
You need to be logged in to leave comments. Login now