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