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