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