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