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