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