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