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