##// END OF EJS Templates
record: fix display of non-ASCII names in chunk selection...
Nikolaj Sjujskij -
r17566:cd73bbc9 default
parent child Browse files
Show More
@@ -1,669 +1,669 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, proc))
36 - ('range', (-start,len, +start,len, proc))
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 shouldn'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 '%s'?") % 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 '%s'?") % (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 # make sure username is set before going interactive
499 # make sure username is set before going interactive
500 ui.username()
500 ui.username()
501
501
502 def recordfunc(ui, repo, message, match, opts):
502 def recordfunc(ui, repo, message, match, opts):
503 """This is generic record driver.
503 """This is generic record driver.
504
504
505 Its job is to interactively filter local changes, and
505 Its job is to interactively filter local changes, and
506 accordingly prepare working directory into a state in which the
506 accordingly prepare working directory into a state in which the
507 job can be delegated to a non-interactive commit command such as
507 job can be delegated to a non-interactive commit command such as
508 'commit' or 'qrefresh'.
508 'commit' or 'qrefresh'.
509
509
510 After the actual job is done by non-interactive command, the
510 After the actual job is done by non-interactive command, the
511 working directory is restored to its original state.
511 working directory is restored to its original state.
512
512
513 In the end we'll record interesting changes, and everything else
513 In the end we'll record interesting changes, and everything else
514 will be left in place, so the user can continue working.
514 will be left in place, so the user can continue working.
515 """
515 """
516
516
517 merge = len(repo[None].parents()) > 1
517 merge = len(repo[None].parents()) > 1
518 if merge:
518 if merge:
519 raise util.Abort(_('cannot partially commit a merge '
519 raise util.Abort(_('cannot partially commit a merge '
520 '(use "hg commit" instead)'))
520 '(use "hg commit" instead)'))
521
521
522 changes = repo.status(match=match)[:3]
522 changes = repo.status(match=match)[:3]
523 diffopts = mdiff.diffopts(
523 diffopts = mdiff.diffopts(
524 git=True, nodates=True,
524 git=True, nodates=True,
525 ignorews=opts.get('ignore_all_space'),
525 ignorews=opts.get('ignore_all_space'),
526 ignorewsamount=opts.get('ignore_space_change'),
526 ignorewsamount=opts.get('ignore_space_change'),
527 ignoreblanklines=opts.get('ignore_blank_lines'))
527 ignoreblanklines=opts.get('ignore_blank_lines'))
528 chunks = patch.diff(repo, changes=changes, opts=diffopts)
528 chunks = patch.diff(repo, changes=changes, opts=diffopts)
529 fp = cStringIO.StringIO()
529 fp = cStringIO.StringIO()
530 fp.write(''.join(chunks))
530 fp.write(''.join(chunks))
531 fp.seek(0)
531 fp.seek(0)
532
532
533 # 1. filter patch, so we have intending-to apply subset of it
533 # 1. filter patch, so we have intending-to apply subset of it
534 chunks = filterpatch(ui, parsepatch(fp))
534 chunks = filterpatch(ui, parsepatch(fp))
535 del fp
535 del fp
536
536
537 contenders = set()
537 contenders = set()
538 for h in chunks:
538 for h in chunks:
539 try:
539 try:
540 contenders.update(set(h.files()))
540 contenders.update(set(h.files()))
541 except AttributeError:
541 except AttributeError:
542 pass
542 pass
543
543
544 changed = changes[0] + changes[1] + changes[2]
544 changed = changes[0] + changes[1] + changes[2]
545 newfiles = [f for f in changed if f in contenders]
545 newfiles = [f for f in changed if f in contenders]
546 if not newfiles:
546 if not newfiles:
547 ui.status(_('no changes to record\n'))
547 ui.status(_('no changes to record\n'))
548 return 0
548 return 0
549
549
550 modified = set(changes[0])
550 modified = set(changes[0])
551
551
552 # 2. backup changed files, so we can restore them in the end
552 # 2. backup changed files, so we can restore them in the end
553 if backupall:
553 if backupall:
554 tobackup = changed
554 tobackup = changed
555 else:
555 else:
556 tobackup = [f for f in newfiles if f in modified]
556 tobackup = [f for f in newfiles if f in modified]
557
557
558 backups = {}
558 backups = {}
559 if tobackup:
559 if tobackup:
560 backupdir = repo.join('record-backups')
560 backupdir = repo.join('record-backups')
561 try:
561 try:
562 os.mkdir(backupdir)
562 os.mkdir(backupdir)
563 except OSError, err:
563 except OSError, err:
564 if err.errno != errno.EEXIST:
564 if err.errno != errno.EEXIST:
565 raise
565 raise
566 try:
566 try:
567 # backup continues
567 # backup continues
568 for f in tobackup:
568 for f in tobackup:
569 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
569 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
570 dir=backupdir)
570 dir=backupdir)
571 os.close(fd)
571 os.close(fd)
572 ui.debug('backup %r as %r\n' % (f, tmpname))
572 ui.debug('backup %r as %r\n' % (f, tmpname))
573 util.copyfile(repo.wjoin(f), tmpname)
573 util.copyfile(repo.wjoin(f), tmpname)
574 shutil.copystat(repo.wjoin(f), tmpname)
574 shutil.copystat(repo.wjoin(f), tmpname)
575 backups[f] = tmpname
575 backups[f] = tmpname
576
576
577 fp = cStringIO.StringIO()
577 fp = cStringIO.StringIO()
578 for c in chunks:
578 for c in chunks:
579 if c.filename() in backups:
579 if c.filename() in backups:
580 c.write(fp)
580 c.write(fp)
581 dopatch = fp.tell()
581 dopatch = fp.tell()
582 fp.seek(0)
582 fp.seek(0)
583
583
584 # 3a. apply filtered patch to clean repo (clean)
584 # 3a. apply filtered patch to clean repo (clean)
585 if backups:
585 if backups:
586 hg.revert(repo, repo.dirstate.p1(),
586 hg.revert(repo, repo.dirstate.p1(),
587 lambda key: key in backups)
587 lambda key: key in backups)
588
588
589 # 3b. (apply)
589 # 3b. (apply)
590 if dopatch:
590 if dopatch:
591 try:
591 try:
592 ui.debug('applying patch\n')
592 ui.debug('applying patch\n')
593 ui.debug(fp.getvalue())
593 ui.debug(fp.getvalue())
594 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
594 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
595 except patch.PatchError, err:
595 except patch.PatchError, err:
596 raise util.Abort(str(err))
596 raise util.Abort(str(err))
597 del fp
597 del fp
598
598
599 # 4. We prepared working directory according to filtered
599 # 4. We prepared working directory according to filtered
600 # patch. Now is the time to delegate the job to
600 # patch. Now is the time to delegate the job to
601 # commit/qrefresh or the like!
601 # commit/qrefresh or the like!
602
602
603 # it is important to first chdir to repo root -- we'll call
603 # it is important to first chdir to repo root -- we'll call
604 # a highlevel command with list of pathnames relative to
604 # a highlevel command with list of pathnames relative to
605 # repo root
605 # repo root
606 cwd = os.getcwd()
606 cwd = os.getcwd()
607 os.chdir(repo.root)
607 os.chdir(repo.root)
608 try:
608 try:
609 commitfunc(ui, repo, *newfiles, **opts)
609 commitfunc(ui, repo, *newfiles, **opts)
610 finally:
610 finally:
611 os.chdir(cwd)
611 os.chdir(cwd)
612
612
613 return 0
613 return 0
614 finally:
614 finally:
615 # 5. finally restore backed-up files
615 # 5. finally restore backed-up files
616 try:
616 try:
617 for realname, tmpname in backups.iteritems():
617 for realname, tmpname in backups.iteritems():
618 ui.debug('restoring %r to %r\n' % (tmpname, realname))
618 ui.debug('restoring %r to %r\n' % (tmpname, realname))
619 util.copyfile(tmpname, repo.wjoin(realname))
619 util.copyfile(tmpname, repo.wjoin(realname))
620 # Our calls to copystat() here and above are a
620 # Our calls to copystat() here and above are a
621 # hack to trick any editors that have f open that
621 # hack to trick any editors that have f open that
622 # we haven't modified them.
622 # we haven't modified them.
623 #
623 #
624 # Also note that this racy as an editor could
624 # Also note that this racy as an editor could
625 # notice the file's mtime before we've finished
625 # notice the file's mtime before we've finished
626 # writing it.
626 # writing it.
627 shutil.copystat(tmpname, repo.wjoin(realname))
627 shutil.copystat(tmpname, repo.wjoin(realname))
628 os.unlink(tmpname)
628 os.unlink(tmpname)
629 if tobackup:
629 if tobackup:
630 os.rmdir(backupdir)
630 os.rmdir(backupdir)
631 except OSError:
631 except OSError:
632 pass
632 pass
633
633
634 # wrap ui.write so diff output can be labeled/colorized
634 # wrap ui.write so diff output can be labeled/colorized
635 def wrapwrite(orig, *args, **kw):
635 def wrapwrite(orig, *args, **kw):
636 label = kw.pop('label', '')
636 label = kw.pop('label', '')
637 for chunk, l in patch.difflabel(lambda: args):
637 for chunk, l in patch.difflabel(lambda: args):
638 orig(chunk, label=label + l)
638 orig(chunk, label=label + l)
639 oldwrite = ui.write
639 oldwrite = ui.write
640 extensions.wrapfunction(ui, 'write', wrapwrite)
640 extensions.wrapfunction(ui, 'write', wrapwrite)
641 try:
641 try:
642 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
642 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
643 finally:
643 finally:
644 ui.write = oldwrite
644 ui.write = oldwrite
645
645
646 cmdtable["qrecord"] = \
646 cmdtable["qrecord"] = \
647 (qrecord, [], # placeholder until mq is available
647 (qrecord, [], # placeholder until mq is available
648 _('hg qrecord [OPTION]... PATCH [FILE]...'))
648 _('hg qrecord [OPTION]... PATCH [FILE]...'))
649
649
650 def uisetup(ui):
650 def uisetup(ui):
651 try:
651 try:
652 mq = extensions.find('mq')
652 mq = extensions.find('mq')
653 except KeyError:
653 except KeyError:
654 return
654 return
655
655
656 cmdtable["qrecord"] = \
656 cmdtable["qrecord"] = \
657 (qrecord,
657 (qrecord,
658 # same options as qnew, but copy them so we don't get
658 # same options as qnew, but copy them so we don't get
659 # -i/--interactive for qrecord and add white space diff options
659 # -i/--interactive for qrecord and add white space diff options
660 mq.cmdtable['^qnew'][1][:] + diffopts,
660 mq.cmdtable['^qnew'][1][:] + diffopts,
661 _('hg qrecord [OPTION]... PATCH [FILE]...'))
661 _('hg qrecord [OPTION]... PATCH [FILE]...'))
662
662
663 _wrapcmd('qnew', mq.cmdtable, qnew, _("interactively record a new patch"))
663 _wrapcmd('qnew', mq.cmdtable, qnew, _("interactively record a new patch"))
664 _wrapcmd('qrefresh', mq.cmdtable, qrefresh,
664 _wrapcmd('qrefresh', mq.cmdtable, qrefresh,
665 _("interactively select changes to refresh"))
665 _("interactively select changes to refresh"))
666
666
667 def _wrapcmd(cmd, table, wrapfn, msg):
667 def _wrapcmd(cmd, table, wrapfn, msg):
668 entry = extensions.wrapcommand(table, cmd, wrapfn)
668 entry = extensions.wrapcommand(table, cmd, wrapfn)
669 entry[1].append(('i', 'interactive', None, msg))
669 entry[1].append(('i', 'interactive', None, msg))
General Comments 0
You need to be logged in to leave comments. Login now