##// END OF EJS Templates
record: refactor record into generic record driver...
Kirill Smelkov -
r5827:0c29977b default
parent child Browse files
Show More
@@ -1,450 +1,485 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'''
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, 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):
362 commands.commit(ui, repo, *pats, **opts)
363
364 dorecord(ui, repo, record_commiter, *pats, **opts)
365
366
367 def dorecord(ui, repo, committer, *pats, **opts):
361 if not ui.interactive:
368 if not ui.interactive:
362 raise util.Abort(_('running non-interactively, use commit instead'))
369 raise util.Abort(_('running non-interactively, use commit instead'))
363
370
364 def recordfunc(ui, repo, files, message, match, opts):
371 def recordfunc(ui, repo, files, message, match, opts):
372 """This is generic record driver.
373
374 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
376 non-interactive commit command such as 'commit' or 'qrefresh'.
377
378 After the actual job is done by non-interactive command, working dir
379 state is restored to original.
380
381 In the end we'll record intresting changes, and everything else will be
382 left in place, so the user can continue his work.
383 """
365 if files:
384 if files:
366 changes = None
385 changes = None
367 else:
386 else:
368 changes = repo.status(files=files, match=match)[:5]
387 changes = repo.status(files=files, match=match)[:5]
369 modified, added, removed = changes[:3]
388 modified, added, removed = changes[:3]
370 files = modified + added + removed
389 files = modified + added + removed
371 diffopts = mdiff.diffopts(git=True, nodates=True)
390 diffopts = mdiff.diffopts(git=True, nodates=True)
372 fp = cStringIO.StringIO()
391 fp = cStringIO.StringIO()
373 patch.diff(repo, repo.dirstate.parents()[0], files=files,
392 patch.diff(repo, repo.dirstate.parents()[0], files=files,
374 match=match, changes=changes, opts=diffopts, fp=fp)
393 match=match, changes=changes, opts=diffopts, fp=fp)
375 fp.seek(0)
394 fp.seek(0)
376
395
396 # 1. filter patch, so we have intending-to apply subset of it
377 chunks = filterpatch(ui, parsepatch(fp))
397 chunks = filterpatch(ui, parsepatch(fp))
378 del fp
398 del fp
379
399
380 contenders = {}
400 contenders = {}
381 for h in chunks:
401 for h in chunks:
382 try: contenders.update(dict.fromkeys(h.files()))
402 try: contenders.update(dict.fromkeys(h.files()))
383 except AttributeError: pass
403 except AttributeError: pass
384
404
385 newfiles = [f for f in files if f in contenders]
405 newfiles = [f for f in files if f in contenders]
386
406
387 if not newfiles:
407 if not newfiles:
388 ui.status(_('no changes to record\n'))
408 ui.status(_('no changes to record\n'))
389 return 0
409 return 0
390
410
391 if changes is None:
411 if changes is None:
392 changes = repo.status(files=newfiles, match=match)[:5]
412 changes = repo.status(files=newfiles, match=match)[:5]
393 modified = dict.fromkeys(changes[0])
413 modified = dict.fromkeys(changes[0])
394
414
415 # 2. backup changed files, so we can restore them in the end
395 backups = {}
416 backups = {}
396 backupdir = repo.join('record-backups')
417 backupdir = repo.join('record-backups')
397 try:
418 try:
398 os.mkdir(backupdir)
419 os.mkdir(backupdir)
399 except OSError, err:
420 except OSError, err:
400 if err.errno != errno.EEXIST:
421 if err.errno != errno.EEXIST:
401 raise
422 raise
402 try:
423 try:
424 # backup continues
403 for f in newfiles:
425 for f in newfiles:
404 if f not in modified:
426 if f not in modified:
405 continue
427 continue
406 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
428 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
407 dir=backupdir)
429 dir=backupdir)
408 os.close(fd)
430 os.close(fd)
409 ui.debug('backup %r as %r\n' % (f, tmpname))
431 ui.debug('backup %r as %r\n' % (f, tmpname))
410 util.copyfile(repo.wjoin(f), tmpname)
432 util.copyfile(repo.wjoin(f), tmpname)
411 backups[f] = tmpname
433 backups[f] = tmpname
412
434
413 fp = cStringIO.StringIO()
435 fp = cStringIO.StringIO()
414 for c in chunks:
436 for c in chunks:
415 if c.filename() in backups:
437 if c.filename() in backups:
416 c.write(fp)
438 c.write(fp)
417 dopatch = fp.tell()
439 dopatch = fp.tell()
418 fp.seek(0)
440 fp.seek(0)
419
441
442 # 3a. apply filtered patch to clean repo (clean)
420 if backups:
443 if backups:
421 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
444 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
422
445
446 # 3b. (apply)
423 if dopatch:
447 if dopatch:
424 ui.debug('applying patch\n')
448 ui.debug('applying patch\n')
425 ui.debug(fp.getvalue())
449 ui.debug(fp.getvalue())
426 patch.internalpatch(fp, ui, 1, repo.root)
450 patch.internalpatch(fp, ui, 1, repo.root)
427 del fp
451 del fp
428
452
429 repo.commit(newfiles, message, opts['user'], opts['date'], match,
453 # 4. We prepared working directory according to filtered patch.
430 force_editor=opts.get('force_editor'))
454 # Now is the time to delegate the job to commit/qrefresh or the like!
455
456 # it is important to first chdir to repo root -- we'll call a
457 # highlevel command with list of pathnames relative to repo root
458 cwd = os.getcwd()
459 os.chdir(repo.root)
460 try:
461 committer(ui, repo, newfiles, opts)
462 finally:
463 os.chdir(cwd)
464
431 return 0
465 return 0
432 finally:
466 finally:
467 # 5. finally restore backed-up files
433 try:
468 try:
434 for realname, tmpname in backups.iteritems():
469 for realname, tmpname in backups.iteritems():
435 ui.debug('restoring %r to %r\n' % (tmpname, realname))
470 ui.debug('restoring %r to %r\n' % (tmpname, realname))
436 util.copyfile(tmpname, repo.wjoin(realname))
471 util.copyfile(tmpname, repo.wjoin(realname))
437 os.unlink(tmpname)
472 os.unlink(tmpname)
438 os.rmdir(backupdir)
473 os.rmdir(backupdir)
439 except OSError:
474 except OSError:
440 pass
475 pass
441 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
476 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
442
477
443 cmdtable = {
478 cmdtable = {
444 "record":
479 "record":
445 (record,
480 (record,
446 [('A', 'addremove', None,
481 [('A', 'addremove', None,
447 _('mark new/missing files as added/removed before committing')),
482 _('mark new/missing files as added/removed before committing')),
448 ] + commands.walkopts + commands.commitopts + commands.commitopts2,
483 ] + commands.walkopts + commands.commitopts + commands.commitopts2,
449 _('hg record [OPTION]... [FILE]...')),
484 _('hg record [OPTION]... [FILE]...')),
450 }
485 }
General Comments 0
You need to be logged in to leave comments. Login now