##// END OF EJS Templates
record: docstring typo
Nicolas Dumazet -
r11235:11353675 stable
parent child Browse files
Show More
@@ -1,552 +1,552 b''
1 # record.py
1 # record.py
2 #
2 #
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''commands to interactively select changes for commit/qrefresh'''
8 '''commands to interactively select changes for commit/qrefresh'''
9
9
10 from mercurial.i18n import gettext, _
10 from mercurial.i18n import gettext, _
11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
12 from mercurial import util
12 from mercurial import util
13 import copy, cStringIO, errno, operator, os, re, tempfile
13 import copy, cStringIO, errno, operator, os, re, tempfile
14
14
15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16
16
17 def scanpatch(fp):
17 def scanpatch(fp):
18 """like patch.iterhunks, but yield different events
18 """like patch.iterhunks, but yield different events
19
19
20 - ('file', [header_lines + fromfile + tofile])
20 - ('file', [header_lines + fromfile + tofile])
21 - ('context', [context_lines])
21 - ('context', [context_lines])
22 - ('hunk', [hunk_lines])
22 - ('hunk', [hunk_lines])
23 - ('range', (-start,len, +start,len, diffp))
23 - ('range', (-start,len, +start,len, diffp))
24 """
24 """
25 lr = patch.linereader(fp)
25 lr = patch.linereader(fp)
26
26
27 def scanwhile(first, p):
27 def scanwhile(first, p):
28 """scan lr while predicate holds"""
28 """scan lr while predicate holds"""
29 lines = [first]
29 lines = [first]
30 while True:
30 while True:
31 line = lr.readline()
31 line = lr.readline()
32 if not line:
32 if not line:
33 break
33 break
34 if p(line):
34 if p(line):
35 lines.append(line)
35 lines.append(line)
36 else:
36 else:
37 lr.push(line)
37 lr.push(line)
38 break
38 break
39 return lines
39 return lines
40
40
41 while True:
41 while True:
42 line = lr.readline()
42 line = lr.readline()
43 if not line:
43 if not line:
44 break
44 break
45 if line.startswith('diff --git a/'):
45 if line.startswith('diff --git a/'):
46 def notheader(line):
46 def notheader(line):
47 s = line.split(None, 1)
47 s = line.split(None, 1)
48 return not s or s[0] not in ('---', 'diff')
48 return not s or s[0] not in ('---', 'diff')
49 header = scanwhile(line, notheader)
49 header = scanwhile(line, notheader)
50 fromfile = lr.readline()
50 fromfile = lr.readline()
51 if fromfile.startswith('---'):
51 if fromfile.startswith('---'):
52 tofile = lr.readline()
52 tofile = lr.readline()
53 header += [fromfile, tofile]
53 header += [fromfile, tofile]
54 else:
54 else:
55 lr.push(fromfile)
55 lr.push(fromfile)
56 yield 'file', header
56 yield 'file', header
57 elif line[0] == ' ':
57 elif line[0] == ' ':
58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
59 elif line[0] in '-+':
59 elif line[0] in '-+':
60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
61 else:
61 else:
62 m = lines_re.match(line)
62 m = lines_re.match(line)
63 if m:
63 if m:
64 yield 'range', m.groups()
64 yield 'range', m.groups()
65 else:
65 else:
66 raise patch.PatchError('unknown patch content: %r' % line)
66 raise patch.PatchError('unknown patch content: %r' % line)
67
67
68 class header(object):
68 class header(object):
69 """patch header
69 """patch header
70
70
71 XXX shoudn't we move this to mercurial/patch.py ?
71 XXX shoudn't we move this to mercurial/patch.py ?
72 """
72 """
73 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
73 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
74 allhunks_re = re.compile('(?:index|new file|deleted file) ')
74 allhunks_re = re.compile('(?:index|new file|deleted file) ')
75 pretty_re = re.compile('(?:new file|deleted file) ')
75 pretty_re = re.compile('(?:new file|deleted file) ')
76 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
76 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
77
77
78 def __init__(self, header):
78 def __init__(self, header):
79 self.header = header
79 self.header = header
80 self.hunks = []
80 self.hunks = []
81
81
82 def binary(self):
82 def binary(self):
83 for h in self.header:
83 for h in self.header:
84 if h.startswith('index '):
84 if h.startswith('index '):
85 return True
85 return True
86
86
87 def pretty(self, fp):
87 def pretty(self, fp):
88 for h in self.header:
88 for h in self.header:
89 if h.startswith('index '):
89 if h.startswith('index '):
90 fp.write(_('this modifies a binary file (all or nothing)\n'))
90 fp.write(_('this modifies a binary file (all or nothing)\n'))
91 break
91 break
92 if self.pretty_re.match(h):
92 if self.pretty_re.match(h):
93 fp.write(h)
93 fp.write(h)
94 if self.binary():
94 if self.binary():
95 fp.write(_('this is a binary file\n'))
95 fp.write(_('this is a binary file\n'))
96 break
96 break
97 if h.startswith('---'):
97 if h.startswith('---'):
98 fp.write(_('%d hunks, %d lines changed\n') %
98 fp.write(_('%d hunks, %d lines changed\n') %
99 (len(self.hunks),
99 (len(self.hunks),
100 sum([h.added + h.removed for h in self.hunks])))
100 sum([h.added + h.removed for h in self.hunks])))
101 break
101 break
102 fp.write(h)
102 fp.write(h)
103
103
104 def write(self, fp):
104 def write(self, fp):
105 fp.write(''.join(self.header))
105 fp.write(''.join(self.header))
106
106
107 def allhunks(self):
107 def allhunks(self):
108 for h in self.header:
108 for h in self.header:
109 if self.allhunks_re.match(h):
109 if self.allhunks_re.match(h):
110 return True
110 return True
111
111
112 def files(self):
112 def files(self):
113 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
113 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
114 if fromfile == tofile:
114 if fromfile == tofile:
115 return [fromfile]
115 return [fromfile]
116 return [fromfile, tofile]
116 return [fromfile, tofile]
117
117
118 def filename(self):
118 def filename(self):
119 return self.files()[-1]
119 return self.files()[-1]
120
120
121 def __repr__(self):
121 def __repr__(self):
122 return '<header %s>' % (' '.join(map(repr, self.files())))
122 return '<header %s>' % (' '.join(map(repr, self.files())))
123
123
124 def special(self):
124 def special(self):
125 for h in self.header:
125 for h in self.header:
126 if self.special_re.match(h):
126 if self.special_re.match(h):
127 return True
127 return True
128
128
129 def countchanges(hunk):
129 def countchanges(hunk):
130 """hunk -> (n+,n-)"""
130 """hunk -> (n+,n-)"""
131 add = len([h for h in hunk if h[0] == '+'])
131 add = len([h for h in hunk if h[0] == '+'])
132 rem = len([h for h in hunk if h[0] == '-'])
132 rem = len([h for h in hunk if h[0] == '-'])
133 return add, rem
133 return add, rem
134
134
135 class hunk(object):
135 class hunk(object):
136 """patch hunk
136 """patch hunk
137
137
138 XXX shouldn't we merge this with patch.hunk ?
138 XXX shouldn't we merge this with patch.hunk ?
139 """
139 """
140 maxcontext = 3
140 maxcontext = 3
141
141
142 def __init__(self, header, fromline, toline, proc, before, hunk, after):
142 def __init__(self, header, fromline, toline, proc, before, hunk, after):
143 def trimcontext(number, lines):
143 def trimcontext(number, lines):
144 delta = len(lines) - self.maxcontext
144 delta = len(lines) - self.maxcontext
145 if False and delta > 0:
145 if False and delta > 0:
146 return number + delta, lines[:self.maxcontext]
146 return number + delta, lines[:self.maxcontext]
147 return number, lines
147 return number, lines
148
148
149 self.header = header
149 self.header = header
150 self.fromline, self.before = trimcontext(fromline, before)
150 self.fromline, self.before = trimcontext(fromline, before)
151 self.toline, self.after = trimcontext(toline, after)
151 self.toline, self.after = trimcontext(toline, after)
152 self.proc = proc
152 self.proc = proc
153 self.hunk = hunk
153 self.hunk = hunk
154 self.added, self.removed = countchanges(self.hunk)
154 self.added, self.removed = countchanges(self.hunk)
155
155
156 def write(self, fp):
156 def write(self, fp):
157 delta = len(self.before) + len(self.after)
157 delta = len(self.before) + len(self.after)
158 if self.after and self.after[-1] == '\\ No newline at end of file\n':
158 if self.after and self.after[-1] == '\\ No newline at end of file\n':
159 delta -= 1
159 delta -= 1
160 fromlen = delta + self.removed
160 fromlen = delta + self.removed
161 tolen = delta + self.added
161 tolen = delta + self.added
162 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
162 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
163 (self.fromline, fromlen, self.toline, tolen,
163 (self.fromline, fromlen, self.toline, tolen,
164 self.proc and (' ' + self.proc)))
164 self.proc and (' ' + self.proc)))
165 fp.write(''.join(self.before + self.hunk + self.after))
165 fp.write(''.join(self.before + self.hunk + self.after))
166
166
167 pretty = write
167 pretty = write
168
168
169 def filename(self):
169 def filename(self):
170 return self.header.filename()
170 return self.header.filename()
171
171
172 def __repr__(self):
172 def __repr__(self):
173 return '<hunk %r@%d>' % (self.filename(), self.fromline)
173 return '<hunk %r@%d>' % (self.filename(), self.fromline)
174
174
175 def parsepatch(fp):
175 def parsepatch(fp):
176 """patch -> [] of hunks """
176 """patch -> [] of hunks """
177 class parser(object):
177 class parser(object):
178 """patch parsing state machine"""
178 """patch parsing state machine"""
179 def __init__(self):
179 def __init__(self):
180 self.fromline = 0
180 self.fromline = 0
181 self.toline = 0
181 self.toline = 0
182 self.proc = ''
182 self.proc = ''
183 self.header = None
183 self.header = None
184 self.context = []
184 self.context = []
185 self.before = []
185 self.before = []
186 self.hunk = []
186 self.hunk = []
187 self.stream = []
187 self.stream = []
188
188
189 def addrange(self, (fromstart, fromend, tostart, toend, proc)):
189 def addrange(self, (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 Returns True/False and sets reps_all and resp_file as
278 Returns True/False and sets reps_all and resp_file as
279 appropriate.
279 appropriate.
280 """
280 """
281 if resp_all[0] is not None:
281 if resp_all[0] is not None:
282 return resp_all[0]
282 return resp_all[0]
283 if resp_file[0] is not None:
283 if resp_file[0] is not None:
284 return resp_file[0]
284 return resp_file[0]
285 while True:
285 while True:
286 resps = _('[Ynsfdaq?]')
286 resps = _('[Ynsfdaq?]')
287 choices = (_('&Yes, record this change'),
287 choices = (_('&Yes, record this change'),
288 _('&No, skip this change'),
288 _('&No, skip this change'),
289 _('&Skip remaining changes to this file'),
289 _('&Skip remaining changes to this file'),
290 _('Record remaining changes to this &file'),
290 _('Record remaining changes to this &file'),
291 _('&Done, skip remaining changes and files'),
291 _('&Done, skip remaining changes and files'),
292 _('Record &all changes to all remaining files'),
292 _('Record &all changes to all remaining files'),
293 _('&Quit, recording no changes'),
293 _('&Quit, recording no changes'),
294 _('&?'))
294 _('&?'))
295 r = ui.promptchoice("%s %s" % (query, resps), choices)
295 r = ui.promptchoice("%s %s" % (query, resps), choices)
296 if r == 7: # ?
296 if r == 7: # ?
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:
300 if l:
301 ui.write(l.strip(), '\n')
301 ui.write(l.strip(), '\n')
302 continue
302 continue
303 elif r == 0: # yes
303 elif r == 0: # yes
304 ret = True
304 ret = True
305 elif r == 1: # no
305 elif r == 1: # no
306 ret = False
306 ret = False
307 elif r == 2: # Skip
307 elif r == 2: # Skip
308 ret = resp_file[0] = False
308 ret = resp_file[0] = False
309 elif r == 3: # file (Record remaining)
309 elif r == 3: # file (Record remaining)
310 ret = resp_file[0] = True
310 ret = resp_file[0] = True
311 elif r == 4: # done, skip remaining
311 elif r == 4: # done, skip remaining
312 ret = resp_all[0] = False
312 ret = resp_all[0] = False
313 elif r == 5: # all
313 elif r == 5: # all
314 ret = resp_all[0] = True
314 ret = resp_all[0] = True
315 elif r == 6: # quit
315 elif r == 6: # quit
316 raise util.Abort(_('user quit'))
316 raise util.Abort(_('user quit'))
317 return ret
317 return ret
318 pos, total = 0, len(chunks) - 1
318 pos, total = 0, len(chunks) - 1
319 while chunks:
319 while chunks:
320 pos = total - len(chunks) + 1
320 pos = total - len(chunks) + 1
321 chunk = chunks.pop()
321 chunk = chunks.pop()
322 if isinstance(chunk, header):
322 if isinstance(chunk, header):
323 # new-file mark
323 # new-file mark
324 resp_file = [None]
324 resp_file = [None]
325 fixoffset = 0
325 fixoffset = 0
326 hdr = ''.join(chunk.header)
326 hdr = ''.join(chunk.header)
327 if hdr in seen:
327 if hdr in seen:
328 consumefile()
328 consumefile()
329 continue
329 continue
330 seen.add(hdr)
330 seen.add(hdr)
331 if resp_all[0] is None:
331 if resp_all[0] is None:
332 chunk.pretty(ui)
332 chunk.pretty(ui)
333 r = prompt(_('examine changes to %s?') %
333 r = prompt(_('examine changes to %s?') %
334 _(' and ').join(map(repr, chunk.files())))
334 _(' and ').join(map(repr, chunk.files())))
335 if r:
335 if r:
336 applied[chunk.filename()] = [chunk]
336 applied[chunk.filename()] = [chunk]
337 if chunk.allhunks():
337 if chunk.allhunks():
338 applied[chunk.filename()] += consumefile()
338 applied[chunk.filename()] += consumefile()
339 else:
339 else:
340 consumefile()
340 consumefile()
341 else:
341 else:
342 # new hunk
342 # new hunk
343 if resp_file[0] is None and resp_all[0] is None:
343 if resp_file[0] is None and resp_all[0] is None:
344 chunk.pretty(ui)
344 chunk.pretty(ui)
345 r = total == 1 and prompt(_('record this change to %r?') %
345 r = total == 1 and prompt(_('record this change to %r?') %
346 chunk.filename()) \
346 chunk.filename()) \
347 or prompt(_('record change %d/%d to %r?') %
347 or prompt(_('record change %d/%d to %r?') %
348 (pos, total, chunk.filename()))
348 (pos, total, chunk.filename()))
349 if r:
349 if r:
350 if fixoffset:
350 if fixoffset:
351 chunk = copy.copy(chunk)
351 chunk = copy.copy(chunk)
352 chunk.toline += fixoffset
352 chunk.toline += fixoffset
353 applied[chunk.filename()].append(chunk)
353 applied[chunk.filename()].append(chunk)
354 else:
354 else:
355 fixoffset += chunk.removed - chunk.added
355 fixoffset += chunk.removed - chunk.added
356 return reduce(operator.add, [h for h in applied.itervalues()
356 return reduce(operator.add, [h for h in applied.itervalues()
357 if h[0].special() or len(h) > 1], [])
357 if h[0].special() or len(h) > 1], [])
358
358
359 def record(ui, repo, *pats, **opts):
359 def record(ui, repo, *pats, **opts):
360 '''interactively select changes to commit
360 '''interactively select changes to commit
361
361
362 If a list of files is omitted, all changes reported by "hg status"
362 If a list of files is omitted, all changes reported by "hg status"
363 will be candidates for recording.
363 will be candidates for recording.
364
364
365 See 'hg help dates' for a list of formats valid for -d/--date.
365 See 'hg help dates' for a list of formats valid for -d/--date.
366
366
367 You will be prompted for whether to record changes to each
367 You will be prompted for whether to record changes to each
368 modified file, and for files with multiple changes, for each
368 modified file, and for files with multiple changes, for each
369 change to use. For each query, the following responses are
369 change to use. For each query, the following responses are
370 possible::
370 possible::
371
371
372 y - record this change
372 y - record this change
373 n - skip this change
373 n - skip this change
374
374
375 s - skip remaining changes to this file
375 s - skip remaining changes to this file
376 f - record remaining changes to this file
376 f - record remaining changes to this file
377
377
378 d - done, skip remaining changes and files
378 d - done, skip remaining changes and files
379 a - record all changes to all remaining files
379 a - record all changes to all remaining files
380 q - quit, recording no changes
380 q - quit, recording no changes
381
381
382 ? - display help'''
382 ? - display help'''
383
383
384 dorecord(ui, repo, commands.commit, *pats, **opts)
384 dorecord(ui, repo, commands.commit, *pats, **opts)
385
385
386
386
387 def qrecord(ui, repo, patch, *pats, **opts):
387 def qrecord(ui, repo, patch, *pats, **opts):
388 '''interactively record a new patch
388 '''interactively record a new patch
389
389
390 See 'hg help qnew' & 'hg help record' for more information and
390 See 'hg help qnew' & 'hg help record' for more information and
391 usage.
391 usage.
392 '''
392 '''
393
393
394 try:
394 try:
395 mq = extensions.find('mq')
395 mq = extensions.find('mq')
396 except KeyError:
396 except KeyError:
397 raise util.Abort(_("'mq' extension not loaded"))
397 raise util.Abort(_("'mq' extension not loaded"))
398
398
399 def committomq(ui, repo, *pats, **opts):
399 def committomq(ui, repo, *pats, **opts):
400 mq.new(ui, repo, patch, *pats, **opts)
400 mq.new(ui, repo, patch, *pats, **opts)
401
401
402 opts = opts.copy()
402 opts = opts.copy()
403 opts['force'] = True # always 'qnew -f'
403 opts['force'] = True # always 'qnew -f'
404 dorecord(ui, repo, committomq, *pats, **opts)
404 dorecord(ui, repo, committomq, *pats, **opts)
405
405
406
406
407 def dorecord(ui, repo, commitfunc, *pats, **opts):
407 def dorecord(ui, repo, commitfunc, *pats, **opts):
408 if not ui.interactive():
408 if not ui.interactive():
409 raise util.Abort(_('running non-interactively, use commit instead'))
409 raise util.Abort(_('running non-interactively, use commit instead'))
410
410
411 def recordfunc(ui, repo, message, match, opts):
411 def recordfunc(ui, repo, message, match, opts):
412 """This is generic record driver.
412 """This is generic record driver.
413
413
414 Its job is to interactively filter local changes, and accordingly
414 Its job is to interactively filter local changes, and accordingly
415 prepare working dir into a state, where the job can be delegated to
415 prepare working dir into a state, where the job can be delegated to
416 non-interactive commit command such as 'commit' or 'qrefresh'.
416 non-interactive commit command such as 'commit' or 'qrefresh'.
417
417
418 After the actual job is done by non-interactive command, working dir
418 After the actual job is done by non-interactive command, working dir
419 state is restored to original.
419 state is restored to original.
420
420
421 In the end we'll record intresting changes, and everything else will be
421 In the end we'll record interesting changes, and everything else will be
422 left in place, so the user can continue his work.
422 left in place, so the user can continue his work.
423 """
423 """
424
424
425 changes = repo.status(match=match)[:3]
425 changes = repo.status(match=match)[:3]
426 diffopts = mdiff.diffopts(git=True, nodates=True)
426 diffopts = mdiff.diffopts(git=True, nodates=True)
427 chunks = patch.diff(repo, changes=changes, opts=diffopts)
427 chunks = patch.diff(repo, changes=changes, opts=diffopts)
428 fp = cStringIO.StringIO()
428 fp = cStringIO.StringIO()
429 fp.write(''.join(chunks))
429 fp.write(''.join(chunks))
430 fp.seek(0)
430 fp.seek(0)
431
431
432 # 1. filter patch, so we have intending-to apply subset of it
432 # 1. filter patch, so we have intending-to apply subset of it
433 chunks = filterpatch(ui, parsepatch(fp))
433 chunks = filterpatch(ui, parsepatch(fp))
434 del fp
434 del fp
435
435
436 contenders = set()
436 contenders = set()
437 for h in chunks:
437 for h in chunks:
438 try:
438 try:
439 contenders.update(set(h.files()))
439 contenders.update(set(h.files()))
440 except AttributeError:
440 except AttributeError:
441 pass
441 pass
442
442
443 changed = changes[0] + changes[1] + changes[2]
443 changed = changes[0] + changes[1] + changes[2]
444 newfiles = [f for f in changed if f in contenders]
444 newfiles = [f for f in changed if f in contenders]
445 if not newfiles:
445 if not newfiles:
446 ui.status(_('no changes to record\n'))
446 ui.status(_('no changes to record\n'))
447 return 0
447 return 0
448
448
449 modified = set(changes[0])
449 modified = set(changes[0])
450
450
451 # 2. backup changed files, so we can restore them in the end
451 # 2. backup changed files, so we can restore them in the end
452 backups = {}
452 backups = {}
453 backupdir = repo.join('record-backups')
453 backupdir = repo.join('record-backups')
454 try:
454 try:
455 os.mkdir(backupdir)
455 os.mkdir(backupdir)
456 except OSError, err:
456 except OSError, err:
457 if err.errno != errno.EEXIST:
457 if err.errno != errno.EEXIST:
458 raise
458 raise
459 try:
459 try:
460 # backup continues
460 # backup continues
461 for f in newfiles:
461 for f in newfiles:
462 if f not in modified:
462 if f not in modified:
463 continue
463 continue
464 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
464 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
465 dir=backupdir)
465 dir=backupdir)
466 os.close(fd)
466 os.close(fd)
467 ui.debug('backup %r as %r\n' % (f, tmpname))
467 ui.debug('backup %r as %r\n' % (f, tmpname))
468 util.copyfile(repo.wjoin(f), tmpname)
468 util.copyfile(repo.wjoin(f), tmpname)
469 backups[f] = tmpname
469 backups[f] = tmpname
470
470
471 fp = cStringIO.StringIO()
471 fp = cStringIO.StringIO()
472 for c in chunks:
472 for c in chunks:
473 if c.filename() in backups:
473 if c.filename() in backups:
474 c.write(fp)
474 c.write(fp)
475 dopatch = fp.tell()
475 dopatch = fp.tell()
476 fp.seek(0)
476 fp.seek(0)
477
477
478 # 3a. apply filtered patch to clean repo (clean)
478 # 3a. apply filtered patch to clean repo (clean)
479 if backups:
479 if backups:
480 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
480 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
481
481
482 # 3b. (apply)
482 # 3b. (apply)
483 if dopatch:
483 if dopatch:
484 try:
484 try:
485 ui.debug('applying patch\n')
485 ui.debug('applying patch\n')
486 ui.debug(fp.getvalue())
486 ui.debug(fp.getvalue())
487 pfiles = {}
487 pfiles = {}
488 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
488 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
489 eolmode=None)
489 eolmode=None)
490 patch.updatedir(ui, repo, pfiles)
490 patch.updatedir(ui, repo, pfiles)
491 except patch.PatchError, err:
491 except patch.PatchError, err:
492 s = str(err)
492 s = str(err)
493 if s:
493 if s:
494 raise util.Abort(s)
494 raise util.Abort(s)
495 else:
495 else:
496 raise util.Abort(_('patch failed to apply'))
496 raise util.Abort(_('patch failed to apply'))
497 del fp
497 del fp
498
498
499 # 4. We prepared working directory according to filtered patch.
499 # 4. We prepared working directory according to filtered patch.
500 # Now is the time to delegate the job to commit/qrefresh or the like!
500 # Now is the time to delegate the job to commit/qrefresh or the like!
501
501
502 # it is important to first chdir to repo root -- we'll call a
502 # it is important to first chdir to repo root -- we'll call a
503 # highlevel command with list of pathnames relative to repo root
503 # highlevel command with list of pathnames relative to repo root
504 cwd = os.getcwd()
504 cwd = os.getcwd()
505 os.chdir(repo.root)
505 os.chdir(repo.root)
506 try:
506 try:
507 commitfunc(ui, repo, *newfiles, **opts)
507 commitfunc(ui, repo, *newfiles, **opts)
508 finally:
508 finally:
509 os.chdir(cwd)
509 os.chdir(cwd)
510
510
511 return 0
511 return 0
512 finally:
512 finally:
513 # 5. finally restore backed-up files
513 # 5. finally restore backed-up files
514 try:
514 try:
515 for realname, tmpname in backups.iteritems():
515 for realname, tmpname in backups.iteritems():
516 ui.debug('restoring %r to %r\n' % (tmpname, realname))
516 ui.debug('restoring %r to %r\n' % (tmpname, realname))
517 util.copyfile(tmpname, repo.wjoin(realname))
517 util.copyfile(tmpname, repo.wjoin(realname))
518 os.unlink(tmpname)
518 os.unlink(tmpname)
519 os.rmdir(backupdir)
519 os.rmdir(backupdir)
520 except OSError:
520 except OSError:
521 pass
521 pass
522 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
522 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
523
523
524 cmdtable = {
524 cmdtable = {
525 "record":
525 "record":
526 (record,
526 (record,
527
527
528 # add commit options
528 # add commit options
529 commands.table['^commit|ci'][1],
529 commands.table['^commit|ci'][1],
530
530
531 _('hg record [OPTION]... [FILE]...')),
531 _('hg record [OPTION]... [FILE]...')),
532 }
532 }
533
533
534
534
535 def uisetup(ui):
535 def uisetup(ui):
536 try:
536 try:
537 mq = extensions.find('mq')
537 mq = extensions.find('mq')
538 except KeyError:
538 except KeyError:
539 return
539 return
540
540
541 qcmdtable = {
541 qcmdtable = {
542 "qrecord":
542 "qrecord":
543 (qrecord,
543 (qrecord,
544
544
545 # add qnew options, except '--force'
545 # add qnew options, except '--force'
546 [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'],
546 [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'],
547
547
548 _('hg qrecord [OPTION]... PATCH [FILE]...')),
548 _('hg qrecord [OPTION]... PATCH [FILE]...')),
549 }
549 }
550
550
551 cmdtable.update(qcmdtable)
551 cmdtable.update(qcmdtable)
552
552
General Comments 0
You need to be logged in to leave comments. Login now