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