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