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