##// END OF EJS Templates
record: teach parsepatch() about non-git style headers...
Steve Borho -
r13157:82f84010 default
parent child Browse files
Show More
@@ -1,578 +1,583
1 # record.py
1 # record.py
2 #
2 #
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''commands to interactively select changes for commit/qrefresh'''
8 '''commands to interactively select changes for commit/qrefresh'''
9
9
10 from mercurial.i18n import gettext, _
10 from mercurial.i18n import gettext, _
11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
12 from mercurial import util
12 from mercurial import util
13 import copy, cStringIO, errno, os, re, shutil, tempfile
13 import copy, cStringIO, errno, 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/') or line.startswith('diff -r '):
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 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
74 diff_re = re.compile('diff -r .* (.*)$')
74 allhunks_re = re.compile('(?:index|new file|deleted file) ')
75 allhunks_re = re.compile('(?:index|new file|deleted file) ')
75 pretty_re = re.compile('(?:new file|deleted file) ')
76 pretty_re = re.compile('(?:new file|deleted file) ')
76 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
77 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
77
78
78 def __init__(self, header):
79 def __init__(self, header):
79 self.header = header
80 self.header = header
80 self.hunks = []
81 self.hunks = []
81
82
82 def binary(self):
83 def binary(self):
83 for h in self.header:
84 for h in self.header:
84 if h.startswith('index '):
85 if h.startswith('index '):
85 return True
86 return True
86
87
87 def pretty(self, fp):
88 def pretty(self, fp):
88 for h in self.header:
89 for h in self.header:
89 if h.startswith('index '):
90 if h.startswith('index '):
90 fp.write(_('this modifies a binary file (all or nothing)\n'))
91 fp.write(_('this modifies a binary file (all or nothing)\n'))
91 break
92 break
92 if self.pretty_re.match(h):
93 if self.pretty_re.match(h):
93 fp.write(h)
94 fp.write(h)
94 if self.binary():
95 if self.binary():
95 fp.write(_('this is a binary file\n'))
96 fp.write(_('this is a binary file\n'))
96 break
97 break
97 if h.startswith('---'):
98 if h.startswith('---'):
98 fp.write(_('%d hunks, %d lines changed\n') %
99 fp.write(_('%d hunks, %d lines changed\n') %
99 (len(self.hunks),
100 (len(self.hunks),
100 sum([max(h.added, h.removed) for h in self.hunks])))
101 sum([max(h.added, h.removed) for h in self.hunks])))
101 break
102 break
102 fp.write(h)
103 fp.write(h)
103
104
104 def write(self, fp):
105 def write(self, fp):
105 fp.write(''.join(self.header))
106 fp.write(''.join(self.header))
106
107
107 def allhunks(self):
108 def allhunks(self):
108 for h in self.header:
109 for h in self.header:
109 if self.allhunks_re.match(h):
110 if self.allhunks_re.match(h):
110 return True
111 return True
111
112
112 def files(self):
113 def files(self):
113 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
114 match = self.diffgit_re.match(self.header[0])
115 if match:
116 fromfile, tofile = match.groups()
114 if fromfile == tofile:
117 if fromfile == tofile:
115 return [fromfile]
118 return [fromfile]
116 return [fromfile, tofile]
119 return [fromfile, tofile]
120 else:
121 return self.diff_re.match(self.header[0]).groups()
117
122
118 def filename(self):
123 def filename(self):
119 return self.files()[-1]
124 return self.files()[-1]
120
125
121 def __repr__(self):
126 def __repr__(self):
122 return '<header %s>' % (' '.join(map(repr, self.files())))
127 return '<header %s>' % (' '.join(map(repr, self.files())))
123
128
124 def special(self):
129 def special(self):
125 for h in self.header:
130 for h in self.header:
126 if self.special_re.match(h):
131 if self.special_re.match(h):
127 return True
132 return True
128
133
129 def countchanges(hunk):
134 def countchanges(hunk):
130 """hunk -> (n+,n-)"""
135 """hunk -> (n+,n-)"""
131 add = len([h for h in hunk if h[0] == '+'])
136 add = len([h for h in hunk if h[0] == '+'])
132 rem = len([h for h in hunk if h[0] == '-'])
137 rem = len([h for h in hunk if h[0] == '-'])
133 return add, rem
138 return add, rem
134
139
135 class hunk(object):
140 class hunk(object):
136 """patch hunk
141 """patch hunk
137
142
138 XXX shouldn't we merge this with patch.hunk ?
143 XXX shouldn't we merge this with patch.hunk ?
139 """
144 """
140 maxcontext = 3
145 maxcontext = 3
141
146
142 def __init__(self, header, fromline, toline, proc, before, hunk, after):
147 def __init__(self, header, fromline, toline, proc, before, hunk, after):
143 def trimcontext(number, lines):
148 def trimcontext(number, lines):
144 delta = len(lines) - self.maxcontext
149 delta = len(lines) - self.maxcontext
145 if False and delta > 0:
150 if False and delta > 0:
146 return number + delta, lines[:self.maxcontext]
151 return number + delta, lines[:self.maxcontext]
147 return number, lines
152 return number, lines
148
153
149 self.header = header
154 self.header = header
150 self.fromline, self.before = trimcontext(fromline, before)
155 self.fromline, self.before = trimcontext(fromline, before)
151 self.toline, self.after = trimcontext(toline, after)
156 self.toline, self.after = trimcontext(toline, after)
152 self.proc = proc
157 self.proc = proc
153 self.hunk = hunk
158 self.hunk = hunk
154 self.added, self.removed = countchanges(self.hunk)
159 self.added, self.removed = countchanges(self.hunk)
155
160
156 def write(self, fp):
161 def write(self, fp):
157 delta = len(self.before) + len(self.after)
162 delta = len(self.before) + len(self.after)
158 if self.after and self.after[-1] == '\\ No newline at end of file\n':
163 if self.after and self.after[-1] == '\\ No newline at end of file\n':
159 delta -= 1
164 delta -= 1
160 fromlen = delta + self.removed
165 fromlen = delta + self.removed
161 tolen = delta + self.added
166 tolen = delta + self.added
162 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
167 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
163 (self.fromline, fromlen, self.toline, tolen,
168 (self.fromline, fromlen, self.toline, tolen,
164 self.proc and (' ' + self.proc)))
169 self.proc and (' ' + self.proc)))
165 fp.write(''.join(self.before + self.hunk + self.after))
170 fp.write(''.join(self.before + self.hunk + self.after))
166
171
167 pretty = write
172 pretty = write
168
173
169 def filename(self):
174 def filename(self):
170 return self.header.filename()
175 return self.header.filename()
171
176
172 def __repr__(self):
177 def __repr__(self):
173 return '<hunk %r@%d>' % (self.filename(), self.fromline)
178 return '<hunk %r@%d>' % (self.filename(), self.fromline)
174
179
175 def parsepatch(fp):
180 def parsepatch(fp):
176 """patch -> [] of hunks """
181 """patch -> [] of hunks """
177 class parser(object):
182 class parser(object):
178 """patch parsing state machine"""
183 """patch parsing state machine"""
179 def __init__(self):
184 def __init__(self):
180 self.fromline = 0
185 self.fromline = 0
181 self.toline = 0
186 self.toline = 0
182 self.proc = ''
187 self.proc = ''
183 self.header = None
188 self.header = None
184 self.context = []
189 self.context = []
185 self.before = []
190 self.before = []
186 self.hunk = []
191 self.hunk = []
187 self.stream = []
192 self.stream = []
188
193
189 def addrange(self, limits):
194 def addrange(self, limits):
190 fromstart, fromend, tostart, toend, proc = limits
195 fromstart, fromend, tostart, toend, proc = limits
191 self.fromline = int(fromstart)
196 self.fromline = int(fromstart)
192 self.toline = int(tostart)
197 self.toline = int(tostart)
193 self.proc = proc
198 self.proc = proc
194
199
195 def addcontext(self, context):
200 def addcontext(self, context):
196 if self.hunk:
201 if self.hunk:
197 h = hunk(self.header, self.fromline, self.toline, self.proc,
202 h = hunk(self.header, self.fromline, self.toline, self.proc,
198 self.before, self.hunk, context)
203 self.before, self.hunk, context)
199 self.header.hunks.append(h)
204 self.header.hunks.append(h)
200 self.stream.append(h)
205 self.stream.append(h)
201 self.fromline += len(self.before) + h.removed
206 self.fromline += len(self.before) + h.removed
202 self.toline += len(self.before) + h.added
207 self.toline += len(self.before) + h.added
203 self.before = []
208 self.before = []
204 self.hunk = []
209 self.hunk = []
205 self.proc = ''
210 self.proc = ''
206 self.context = context
211 self.context = context
207
212
208 def addhunk(self, hunk):
213 def addhunk(self, hunk):
209 if self.context:
214 if self.context:
210 self.before = self.context
215 self.before = self.context
211 self.context = []
216 self.context = []
212 self.hunk = hunk
217 self.hunk = hunk
213
218
214 def newfile(self, hdr):
219 def newfile(self, hdr):
215 self.addcontext([])
220 self.addcontext([])
216 h = header(hdr)
221 h = header(hdr)
217 self.stream.append(h)
222 self.stream.append(h)
218 self.header = h
223 self.header = h
219
224
220 def finished(self):
225 def finished(self):
221 self.addcontext([])
226 self.addcontext([])
222 return self.stream
227 return self.stream
223
228
224 transitions = {
229 transitions = {
225 'file': {'context': addcontext,
230 'file': {'context': addcontext,
226 'file': newfile,
231 'file': newfile,
227 'hunk': addhunk,
232 'hunk': addhunk,
228 'range': addrange},
233 'range': addrange},
229 'context': {'file': newfile,
234 'context': {'file': newfile,
230 'hunk': addhunk,
235 'hunk': addhunk,
231 'range': addrange},
236 'range': addrange},
232 'hunk': {'context': addcontext,
237 'hunk': {'context': addcontext,
233 'file': newfile,
238 'file': newfile,
234 'range': addrange},
239 'range': addrange},
235 'range': {'context': addcontext,
240 'range': {'context': addcontext,
236 'hunk': addhunk},
241 'hunk': addhunk},
237 }
242 }
238
243
239 p = parser()
244 p = parser()
240
245
241 state = 'context'
246 state = 'context'
242 for newstate, data in scanpatch(fp):
247 for newstate, data in scanpatch(fp):
243 try:
248 try:
244 p.transitions[state][newstate](p, data)
249 p.transitions[state][newstate](p, data)
245 except KeyError:
250 except KeyError:
246 raise patch.PatchError('unhandled transition: %s -> %s' %
251 raise patch.PatchError('unhandled transition: %s -> %s' %
247 (state, newstate))
252 (state, newstate))
248 state = newstate
253 state = newstate
249 return p.finished()
254 return p.finished()
250
255
251 def filterpatch(ui, chunks):
256 def filterpatch(ui, chunks):
252 """Interactively filter patch chunks into applied-only chunks"""
257 """Interactively filter patch chunks into applied-only chunks"""
253 chunks = list(chunks)
258 chunks = list(chunks)
254 chunks.reverse()
259 chunks.reverse()
255 seen = set()
260 seen = set()
256 def consumefile():
261 def consumefile():
257 """fetch next portion from chunks until a 'header' is seen
262 """fetch next portion from chunks until a 'header' is seen
258 NB: header == new-file mark
263 NB: header == new-file mark
259 """
264 """
260 consumed = []
265 consumed = []
261 while chunks:
266 while chunks:
262 if isinstance(chunks[-1], header):
267 if isinstance(chunks[-1], header):
263 break
268 break
264 else:
269 else:
265 consumed.append(chunks.pop())
270 consumed.append(chunks.pop())
266 return consumed
271 return consumed
267
272
268 resp_all = [None] # this two are changed from inside prompt,
273 resp_all = [None] # this two are changed from inside prompt,
269 resp_file = [None] # so can't be usual variables
274 resp_file = [None] # so can't be usual variables
270 applied = {} # 'filename' -> [] of chunks
275 applied = {} # 'filename' -> [] of chunks
271 def prompt(query):
276 def prompt(query):
272 """prompt query, and process base inputs
277 """prompt query, and process base inputs
273
278
274 - y/n for the rest of file
279 - y/n for the rest of file
275 - y/n for the rest
280 - y/n for the rest
276 - ? (help)
281 - ? (help)
277 - q (quit)
282 - q (quit)
278
283
279 Returns True/False and sets reps_all and resp_file as
284 Returns True/False and sets reps_all and resp_file as
280 appropriate.
285 appropriate.
281 """
286 """
282 if resp_all[0] is not None:
287 if resp_all[0] is not None:
283 return resp_all[0]
288 return resp_all[0]
284 if resp_file[0] is not None:
289 if resp_file[0] is not None:
285 return resp_file[0]
290 return resp_file[0]
286 while True:
291 while True:
287 resps = _('[Ynsfdaq?]')
292 resps = _('[Ynsfdaq?]')
288 choices = (_('&Yes, record this change'),
293 choices = (_('&Yes, record this change'),
289 _('&No, skip this change'),
294 _('&No, skip this change'),
290 _('&Skip remaining changes to this file'),
295 _('&Skip remaining changes to this file'),
291 _('Record remaining changes to this &file'),
296 _('Record remaining changes to this &file'),
292 _('&Done, skip remaining changes and files'),
297 _('&Done, skip remaining changes and files'),
293 _('Record &all changes to all remaining files'),
298 _('Record &all changes to all remaining files'),
294 _('&Quit, recording no changes'),
299 _('&Quit, recording no changes'),
295 _('&?'))
300 _('&?'))
296 r = ui.promptchoice("%s %s" % (query, resps), choices)
301 r = ui.promptchoice("%s %s" % (query, resps), choices)
297 ui.write("\n")
302 ui.write("\n")
298 if r == 7: # ?
303 if r == 7: # ?
299 doc = gettext(record.__doc__)
304 doc = gettext(record.__doc__)
300 c = doc.find('::') + 2
305 c = doc.find('::') + 2
301 for l in doc[c:].splitlines():
306 for l in doc[c:].splitlines():
302 if l.startswith(' '):
307 if l.startswith(' '):
303 ui.write(l.strip(), '\n')
308 ui.write(l.strip(), '\n')
304 continue
309 continue
305 elif r == 0: # yes
310 elif r == 0: # yes
306 ret = True
311 ret = True
307 elif r == 1: # no
312 elif r == 1: # no
308 ret = False
313 ret = False
309 elif r == 2: # Skip
314 elif r == 2: # Skip
310 ret = resp_file[0] = False
315 ret = resp_file[0] = False
311 elif r == 3: # file (Record remaining)
316 elif r == 3: # file (Record remaining)
312 ret = resp_file[0] = True
317 ret = resp_file[0] = True
313 elif r == 4: # done, skip remaining
318 elif r == 4: # done, skip remaining
314 ret = resp_all[0] = False
319 ret = resp_all[0] = False
315 elif r == 5: # all
320 elif r == 5: # all
316 ret = resp_all[0] = True
321 ret = resp_all[0] = True
317 elif r == 6: # quit
322 elif r == 6: # quit
318 raise util.Abort(_('user quit'))
323 raise util.Abort(_('user quit'))
319 return ret
324 return ret
320 pos, total = 0, len(chunks) - 1
325 pos, total = 0, len(chunks) - 1
321 while chunks:
326 while chunks:
322 pos = total - len(chunks) + 1
327 pos = total - len(chunks) + 1
323 chunk = chunks.pop()
328 chunk = chunks.pop()
324 if isinstance(chunk, header):
329 if isinstance(chunk, header):
325 # new-file mark
330 # new-file mark
326 resp_file = [None]
331 resp_file = [None]
327 fixoffset = 0
332 fixoffset = 0
328 hdr = ''.join(chunk.header)
333 hdr = ''.join(chunk.header)
329 if hdr in seen:
334 if hdr in seen:
330 consumefile()
335 consumefile()
331 continue
336 continue
332 seen.add(hdr)
337 seen.add(hdr)
333 if resp_all[0] is None:
338 if resp_all[0] is None:
334 chunk.pretty(ui)
339 chunk.pretty(ui)
335 r = prompt(_('examine changes to %s?') %
340 r = prompt(_('examine changes to %s?') %
336 _(' and ').join(map(repr, chunk.files())))
341 _(' and ').join(map(repr, chunk.files())))
337 if r:
342 if r:
338 applied[chunk.filename()] = [chunk]
343 applied[chunk.filename()] = [chunk]
339 if chunk.allhunks():
344 if chunk.allhunks():
340 applied[chunk.filename()] += consumefile()
345 applied[chunk.filename()] += consumefile()
341 else:
346 else:
342 consumefile()
347 consumefile()
343 else:
348 else:
344 # new hunk
349 # new hunk
345 if resp_file[0] is None and resp_all[0] is None:
350 if resp_file[0] is None and resp_all[0] is None:
346 chunk.pretty(ui)
351 chunk.pretty(ui)
347 r = (total == 1
352 r = (total == 1
348 and prompt(_('record this change to %r?') % chunk.filename())
353 and prompt(_('record this change to %r?') % chunk.filename())
349 or prompt(_('record change %d/%d to %r?') %
354 or prompt(_('record change %d/%d to %r?') %
350 (pos, total, chunk.filename())))
355 (pos, total, chunk.filename())))
351 if r:
356 if r:
352 if fixoffset:
357 if fixoffset:
353 chunk = copy.copy(chunk)
358 chunk = copy.copy(chunk)
354 chunk.toline += fixoffset
359 chunk.toline += fixoffset
355 applied[chunk.filename()].append(chunk)
360 applied[chunk.filename()].append(chunk)
356 else:
361 else:
357 fixoffset += chunk.removed - chunk.added
362 fixoffset += chunk.removed - chunk.added
358 return sum([h for h in applied.itervalues()
363 return sum([h for h in applied.itervalues()
359 if h[0].special() or len(h) > 1], [])
364 if h[0].special() or len(h) > 1], [])
360
365
361 def record(ui, repo, *pats, **opts):
366 def record(ui, repo, *pats, **opts):
362 '''interactively select changes to commit
367 '''interactively select changes to commit
363
368
364 If a list of files is omitted, all changes reported by :hg:`status`
369 If a list of files is omitted, all changes reported by :hg:`status`
365 will be candidates for recording.
370 will be candidates for recording.
366
371
367 See :hg:`help dates` for a list of formats valid for -d/--date.
372 See :hg:`help dates` for a list of formats valid for -d/--date.
368
373
369 You will be prompted for whether to record changes to each
374 You will be prompted for whether to record changes to each
370 modified file, and for files with multiple changes, for each
375 modified file, and for files with multiple changes, for each
371 change to use. For each query, the following responses are
376 change to use. For each query, the following responses are
372 possible::
377 possible::
373
378
374 y - record this change
379 y - record this change
375 n - skip this change
380 n - skip this change
376
381
377 s - skip remaining changes to this file
382 s - skip remaining changes to this file
378 f - record remaining changes to this file
383 f - record remaining changes to this file
379
384
380 d - done, skip remaining changes and files
385 d - done, skip remaining changes and files
381 a - record all changes to all remaining files
386 a - record all changes to all remaining files
382 q - quit, recording no changes
387 q - quit, recording no changes
383
388
384 ? - display help
389 ? - display help
385
390
386 This command is not available when committing a merge.'''
391 This command is not available when committing a merge.'''
387
392
388 dorecord(ui, repo, commands.commit, *pats, **opts)
393 dorecord(ui, repo, commands.commit, *pats, **opts)
389
394
390
395
391 def qrecord(ui, repo, patch, *pats, **opts):
396 def qrecord(ui, repo, patch, *pats, **opts):
392 '''interactively record a new patch
397 '''interactively record a new patch
393
398
394 See :hg:`help qnew` & :hg:`help record` for more information and
399 See :hg:`help qnew` & :hg:`help record` for more information and
395 usage.
400 usage.
396 '''
401 '''
397
402
398 try:
403 try:
399 mq = extensions.find('mq')
404 mq = extensions.find('mq')
400 except KeyError:
405 except KeyError:
401 raise util.Abort(_("'mq' extension not loaded"))
406 raise util.Abort(_("'mq' extension not loaded"))
402
407
403 def committomq(ui, repo, *pats, **opts):
408 def committomq(ui, repo, *pats, **opts):
404 mq.new(ui, repo, patch, *pats, **opts)
409 mq.new(ui, repo, patch, *pats, **opts)
405
410
406 opts = opts.copy()
411 opts = opts.copy()
407 opts['force'] = True # always 'qnew -f'
412 opts['force'] = True # always 'qnew -f'
408 dorecord(ui, repo, committomq, *pats, **opts)
413 dorecord(ui, repo, committomq, *pats, **opts)
409
414
410
415
411 def dorecord(ui, repo, commitfunc, *pats, **opts):
416 def dorecord(ui, repo, commitfunc, *pats, **opts):
412 if not ui.interactive():
417 if not ui.interactive():
413 raise util.Abort(_('running non-interactively, use commit instead'))
418 raise util.Abort(_('running non-interactively, use commit instead'))
414
419
415 def recordfunc(ui, repo, message, match, opts):
420 def recordfunc(ui, repo, message, match, opts):
416 """This is generic record driver.
421 """This is generic record driver.
417
422
418 Its job is to interactively filter local changes, and accordingly
423 Its job is to interactively filter local changes, and accordingly
419 prepare working dir into a state, where the job can be delegated to
424 prepare working dir into a state, where the job can be delegated to
420 non-interactive commit command such as 'commit' or 'qrefresh'.
425 non-interactive commit command such as 'commit' or 'qrefresh'.
421
426
422 After the actual job is done by non-interactive command, working dir
427 After the actual job is done by non-interactive command, working dir
423 state is restored to original.
428 state is restored to original.
424
429
425 In the end we'll record interesting changes, and everything else will be
430 In the end we'll record interesting changes, and everything else will be
426 left in place, so the user can continue his work.
431 left in place, so the user can continue his work.
427 """
432 """
428
433
429 merge = len(repo[None].parents()) > 1
434 merge = len(repo[None].parents()) > 1
430 if merge:
435 if merge:
431 raise util.Abort(_('cannot partially commit a merge '
436 raise util.Abort(_('cannot partially commit a merge '
432 '(use "hg commit" instead)'))
437 '(use "hg commit" instead)'))
433
438
434 changes = repo.status(match=match)[:3]
439 changes = repo.status(match=match)[:3]
435 diffopts = mdiff.diffopts(git=True, nodates=True)
440 diffopts = mdiff.diffopts(git=True, nodates=True)
436 chunks = patch.diff(repo, changes=changes, opts=diffopts)
441 chunks = patch.diff(repo, changes=changes, opts=diffopts)
437 fp = cStringIO.StringIO()
442 fp = cStringIO.StringIO()
438 fp.write(''.join(chunks))
443 fp.write(''.join(chunks))
439 fp.seek(0)
444 fp.seek(0)
440
445
441 # 1. filter patch, so we have intending-to apply subset of it
446 # 1. filter patch, so we have intending-to apply subset of it
442 chunks = filterpatch(ui, parsepatch(fp))
447 chunks = filterpatch(ui, parsepatch(fp))
443 del fp
448 del fp
444
449
445 contenders = set()
450 contenders = set()
446 for h in chunks:
451 for h in chunks:
447 try:
452 try:
448 contenders.update(set(h.files()))
453 contenders.update(set(h.files()))
449 except AttributeError:
454 except AttributeError:
450 pass
455 pass
451
456
452 changed = changes[0] + changes[1] + changes[2]
457 changed = changes[0] + changes[1] + changes[2]
453 newfiles = [f for f in changed if f in contenders]
458 newfiles = [f for f in changed if f in contenders]
454 if not newfiles:
459 if not newfiles:
455 ui.status(_('no changes to record\n'))
460 ui.status(_('no changes to record\n'))
456 return 0
461 return 0
457
462
458 modified = set(changes[0])
463 modified = set(changes[0])
459
464
460 # 2. backup changed files, so we can restore them in the end
465 # 2. backup changed files, so we can restore them in the end
461 backups = {}
466 backups = {}
462 backupdir = repo.join('record-backups')
467 backupdir = repo.join('record-backups')
463 try:
468 try:
464 os.mkdir(backupdir)
469 os.mkdir(backupdir)
465 except OSError, err:
470 except OSError, err:
466 if err.errno != errno.EEXIST:
471 if err.errno != errno.EEXIST:
467 raise
472 raise
468 try:
473 try:
469 # backup continues
474 # backup continues
470 for f in newfiles:
475 for f in newfiles:
471 if f not in modified:
476 if f not in modified:
472 continue
477 continue
473 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
478 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
474 dir=backupdir)
479 dir=backupdir)
475 os.close(fd)
480 os.close(fd)
476 ui.debug('backup %r as %r\n' % (f, tmpname))
481 ui.debug('backup %r as %r\n' % (f, tmpname))
477 util.copyfile(repo.wjoin(f), tmpname)
482 util.copyfile(repo.wjoin(f), tmpname)
478 shutil.copystat(repo.wjoin(f), tmpname)
483 shutil.copystat(repo.wjoin(f), tmpname)
479 backups[f] = tmpname
484 backups[f] = tmpname
480
485
481 fp = cStringIO.StringIO()
486 fp = cStringIO.StringIO()
482 for c in chunks:
487 for c in chunks:
483 if c.filename() in backups:
488 if c.filename() in backups:
484 c.write(fp)
489 c.write(fp)
485 dopatch = fp.tell()
490 dopatch = fp.tell()
486 fp.seek(0)
491 fp.seek(0)
487
492
488 # 3a. apply filtered patch to clean repo (clean)
493 # 3a. apply filtered patch to clean repo (clean)
489 if backups:
494 if backups:
490 hg.revert(repo, repo.dirstate.parents()[0],
495 hg.revert(repo, repo.dirstate.parents()[0],
491 lambda key: key in backups)
496 lambda key: key in backups)
492
497
493 # 3b. (apply)
498 # 3b. (apply)
494 if dopatch:
499 if dopatch:
495 try:
500 try:
496 ui.debug('applying patch\n')
501 ui.debug('applying patch\n')
497 ui.debug(fp.getvalue())
502 ui.debug(fp.getvalue())
498 pfiles = {}
503 pfiles = {}
499 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
504 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
500 eolmode=None)
505 eolmode=None)
501 cmdutil.updatedir(ui, repo, pfiles)
506 cmdutil.updatedir(ui, repo, pfiles)
502 except patch.PatchError, err:
507 except patch.PatchError, err:
503 raise util.Abort(str(err))
508 raise util.Abort(str(err))
504 del fp
509 del fp
505
510
506 # 4. We prepared working directory according to filtered patch.
511 # 4. We prepared working directory according to filtered patch.
507 # Now is the time to delegate the job to commit/qrefresh or the like!
512 # Now is the time to delegate the job to commit/qrefresh or the like!
508
513
509 # it is important to first chdir to repo root -- we'll call a
514 # it is important to first chdir to repo root -- we'll call a
510 # highlevel command with list of pathnames relative to repo root
515 # highlevel command with list of pathnames relative to repo root
511 cwd = os.getcwd()
516 cwd = os.getcwd()
512 os.chdir(repo.root)
517 os.chdir(repo.root)
513 try:
518 try:
514 commitfunc(ui, repo, *newfiles, **opts)
519 commitfunc(ui, repo, *newfiles, **opts)
515 finally:
520 finally:
516 os.chdir(cwd)
521 os.chdir(cwd)
517
522
518 return 0
523 return 0
519 finally:
524 finally:
520 # 5. finally restore backed-up files
525 # 5. finally restore backed-up files
521 try:
526 try:
522 for realname, tmpname in backups.iteritems():
527 for realname, tmpname in backups.iteritems():
523 ui.debug('restoring %r to %r\n' % (tmpname, realname))
528 ui.debug('restoring %r to %r\n' % (tmpname, realname))
524 util.copyfile(tmpname, repo.wjoin(realname))
529 util.copyfile(tmpname, repo.wjoin(realname))
525 # Our calls to copystat() here and above are a
530 # Our calls to copystat() here and above are a
526 # hack to trick any editors that have f open that
531 # hack to trick any editors that have f open that
527 # we haven't modified them.
532 # we haven't modified them.
528 #
533 #
529 # Also note that this racy as an editor could
534 # Also note that this racy as an editor could
530 # notice the file's mtime before we've finished
535 # notice the file's mtime before we've finished
531 # writing it.
536 # writing it.
532 shutil.copystat(tmpname, repo.wjoin(realname))
537 shutil.copystat(tmpname, repo.wjoin(realname))
533 os.unlink(tmpname)
538 os.unlink(tmpname)
534 os.rmdir(backupdir)
539 os.rmdir(backupdir)
535 except OSError:
540 except OSError:
536 pass
541 pass
537
542
538 # wrap ui.write so diff output can be labeled/colorized
543 # wrap ui.write so diff output can be labeled/colorized
539 def wrapwrite(orig, *args, **kw):
544 def wrapwrite(orig, *args, **kw):
540 label = kw.pop('label', '')
545 label = kw.pop('label', '')
541 for chunk, l in patch.difflabel(lambda: args):
546 for chunk, l in patch.difflabel(lambda: args):
542 orig(chunk, label=label + l)
547 orig(chunk, label=label + l)
543 oldwrite = ui.write
548 oldwrite = ui.write
544 extensions.wrapfunction(ui, 'write', wrapwrite)
549 extensions.wrapfunction(ui, 'write', wrapwrite)
545 try:
550 try:
546 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
551 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
547 finally:
552 finally:
548 ui.write = oldwrite
553 ui.write = oldwrite
549
554
550 cmdtable = {
555 cmdtable = {
551 "record":
556 "record":
552 (record,
557 (record,
553
558
554 # add commit options
559 # add commit options
555 commands.table['^commit|ci'][1],
560 commands.table['^commit|ci'][1],
556
561
557 _('hg record [OPTION]... [FILE]...')),
562 _('hg record [OPTION]... [FILE]...')),
558 }
563 }
559
564
560
565
561 def uisetup(ui):
566 def uisetup(ui):
562 try:
567 try:
563 mq = extensions.find('mq')
568 mq = extensions.find('mq')
564 except KeyError:
569 except KeyError:
565 return
570 return
566
571
567 qcmdtable = {
572 qcmdtable = {
568 "qrecord":
573 "qrecord":
569 (qrecord,
574 (qrecord,
570
575
571 # add qnew options, except '--force'
576 # add qnew options, except '--force'
572 [opt for opt in mq.cmdtable['^qnew'][1] if opt[1] != 'force'],
577 [opt for opt in mq.cmdtable['^qnew'][1] if opt[1] != 'force'],
573
578
574 _('hg qrecord [OPTION]... PATCH [FILE]...')),
579 _('hg qrecord [OPTION]... PATCH [FILE]...')),
575 }
580 }
576
581
577 cmdtable.update(qcmdtable)
582 cmdtable.update(qcmdtable)
578
583
General Comments 0
You need to be logged in to leave comments. Login now