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