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