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