##// END OF EJS Templates
record: better formatting for qrecord help
Martin Geisler -
r8348:c9ab8f44 default
parent child Browse files
Show More
@@ -1,546 +1,547 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, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''interactive change selection during commit or qrefresh'''
8 '''interactive change selection during commit or 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, operator, os, re, tempfile
13 import copy, cStringIO, errno, operator, os, re, tempfile
14
14
15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16
16
17 def scanpatch(fp):
17 def scanpatch(fp):
18 """like patch.iterhunks, but yield different events
18 """like patch.iterhunks, but yield different events
19
19
20 - ('file', [header_lines + fromfile + tofile])
20 - ('file', [header_lines + fromfile + tofile])
21 - ('context', [context_lines])
21 - ('context', [context_lines])
22 - ('hunk', [hunk_lines])
22 - ('hunk', [hunk_lines])
23 - ('range', (-start,len, +start,len, diffp))
23 - ('range', (-start,len, +start,len, diffp))
24 """
24 """
25 lr = patch.linereader(fp)
25 lr = patch.linereader(fp)
26
26
27 def scanwhile(first, p):
27 def scanwhile(first, p):
28 """scan lr while predicate holds"""
28 """scan lr while predicate holds"""
29 lines = [first]
29 lines = [first]
30 while True:
30 while True:
31 line = lr.readline()
31 line = lr.readline()
32 if not line:
32 if not line:
33 break
33 break
34 if p(line):
34 if p(line):
35 lines.append(line)
35 lines.append(line)
36 else:
36 else:
37 lr.push(line)
37 lr.push(line)
38 break
38 break
39 return lines
39 return lines
40
40
41 while True:
41 while True:
42 line = lr.readline()
42 line = lr.readline()
43 if not line:
43 if not line:
44 break
44 break
45 if line.startswith('diff --git a/'):
45 if line.startswith('diff --git a/'):
46 def notheader(line):
46 def notheader(line):
47 s = line.split(None, 1)
47 s = line.split(None, 1)
48 return not s or s[0] not in ('---', 'diff')
48 return not s or s[0] not in ('---', 'diff')
49 header = scanwhile(line, notheader)
49 header = scanwhile(line, notheader)
50 fromfile = lr.readline()
50 fromfile = lr.readline()
51 if fromfile.startswith('---'):
51 if fromfile.startswith('---'):
52 tofile = lr.readline()
52 tofile = lr.readline()
53 header += [fromfile, tofile]
53 header += [fromfile, tofile]
54 else:
54 else:
55 lr.push(fromfile)
55 lr.push(fromfile)
56 yield 'file', header
56 yield 'file', header
57 elif line[0] == ' ':
57 elif line[0] == ' ':
58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
59 elif line[0] in '-+':
59 elif line[0] in '-+':
60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
61 else:
61 else:
62 m = lines_re.match(line)
62 m = lines_re.match(line)
63 if m:
63 if m:
64 yield 'range', m.groups()
64 yield 'range', m.groups()
65 else:
65 else:
66 raise patch.PatchError('unknown patch content: %r' % line)
66 raise patch.PatchError('unknown patch content: %r' % line)
67
67
68 class header(object):
68 class header(object):
69 """patch header
69 """patch header
70
70
71 XXX shoudn't we move this to mercurial/patch.py ?
71 XXX shoudn't we move this to mercurial/patch.py ?
72 """
72 """
73 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
73 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
74 allhunks_re = re.compile('(?:index|new file|deleted file) ')
74 allhunks_re = re.compile('(?:index|new file|deleted file) ')
75 pretty_re = re.compile('(?:new file|deleted file) ')
75 pretty_re = re.compile('(?:new file|deleted file) ')
76 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
76 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
77
77
78 def __init__(self, header):
78 def __init__(self, header):
79 self.header = header
79 self.header = header
80 self.hunks = []
80 self.hunks = []
81
81
82 def binary(self):
82 def binary(self):
83 for h in self.header:
83 for h in self.header:
84 if h.startswith('index '):
84 if h.startswith('index '):
85 return True
85 return True
86
86
87 def pretty(self, fp):
87 def pretty(self, fp):
88 for h in self.header:
88 for h in self.header:
89 if h.startswith('index '):
89 if h.startswith('index '):
90 fp.write(_('this modifies a binary file (all or nothing)\n'))
90 fp.write(_('this modifies a binary file (all or nothing)\n'))
91 break
91 break
92 if self.pretty_re.match(h):
92 if self.pretty_re.match(h):
93 fp.write(h)
93 fp.write(h)
94 if self.binary():
94 if self.binary():
95 fp.write(_('this is a binary file\n'))
95 fp.write(_('this is a binary file\n'))
96 break
96 break
97 if h.startswith('---'):
97 if h.startswith('---'):
98 fp.write(_('%d hunks, %d lines changed\n') %
98 fp.write(_('%d hunks, %d lines changed\n') %
99 (len(self.hunks),
99 (len(self.hunks),
100 sum([h.added + h.removed for h in self.hunks])))
100 sum([h.added + h.removed for h in self.hunks])))
101 break
101 break
102 fp.write(h)
102 fp.write(h)
103
103
104 def write(self, fp):
104 def write(self, fp):
105 fp.write(''.join(self.header))
105 fp.write(''.join(self.header))
106
106
107 def allhunks(self):
107 def allhunks(self):
108 for h in self.header:
108 for h in self.header:
109 if self.allhunks_re.match(h):
109 if self.allhunks_re.match(h):
110 return True
110 return True
111
111
112 def files(self):
112 def files(self):
113 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
113 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
114 if fromfile == tofile:
114 if fromfile == tofile:
115 return [fromfile]
115 return [fromfile]
116 return [fromfile, tofile]
116 return [fromfile, tofile]
117
117
118 def filename(self):
118 def filename(self):
119 return self.files()[-1]
119 return self.files()[-1]
120
120
121 def __repr__(self):
121 def __repr__(self):
122 return '<header %s>' % (' '.join(map(repr, self.files())))
122 return '<header %s>' % (' '.join(map(repr, self.files())))
123
123
124 def special(self):
124 def special(self):
125 for h in self.header:
125 for h in self.header:
126 if self.special_re.match(h):
126 if self.special_re.match(h):
127 return True
127 return True
128
128
129 def countchanges(hunk):
129 def countchanges(hunk):
130 """hunk -> (n+,n-)"""
130 """hunk -> (n+,n-)"""
131 add = len([h for h in hunk if h[0] == '+'])
131 add = len([h for h in hunk if h[0] == '+'])
132 rem = len([h for h in hunk if h[0] == '-'])
132 rem = len([h for h in hunk if h[0] == '-'])
133 return add, rem
133 return add, rem
134
134
135 class hunk(object):
135 class hunk(object):
136 """patch hunk
136 """patch hunk
137
137
138 XXX shouldn't we merge this with patch.hunk ?
138 XXX shouldn't we merge this with patch.hunk ?
139 """
139 """
140 maxcontext = 3
140 maxcontext = 3
141
141
142 def __init__(self, header, fromline, toline, proc, before, hunk, after):
142 def __init__(self, header, fromline, toline, proc, before, hunk, after):
143 def trimcontext(number, lines):
143 def trimcontext(number, lines):
144 delta = len(lines) - self.maxcontext
144 delta = len(lines) - self.maxcontext
145 if False and delta > 0:
145 if False and delta > 0:
146 return number + delta, lines[:self.maxcontext]
146 return number + delta, lines[:self.maxcontext]
147 return number, lines
147 return number, lines
148
148
149 self.header = header
149 self.header = header
150 self.fromline, self.before = trimcontext(fromline, before)
150 self.fromline, self.before = trimcontext(fromline, before)
151 self.toline, self.after = trimcontext(toline, after)
151 self.toline, self.after = trimcontext(toline, after)
152 self.proc = proc
152 self.proc = proc
153 self.hunk = hunk
153 self.hunk = hunk
154 self.added, self.removed = countchanges(self.hunk)
154 self.added, self.removed = countchanges(self.hunk)
155
155
156 def write(self, fp):
156 def write(self, fp):
157 delta = len(self.before) + len(self.after)
157 delta = len(self.before) + len(self.after)
158 if self.after and self.after[-1] == '\\ No newline at end of file\n':
158 if self.after and self.after[-1] == '\\ No newline at end of file\n':
159 delta -= 1
159 delta -= 1
160 fromlen = delta + self.removed
160 fromlen = delta + self.removed
161 tolen = delta + self.added
161 tolen = delta + self.added
162 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
162 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
163 (self.fromline, fromlen, self.toline, tolen,
163 (self.fromline, fromlen, self.toline, tolen,
164 self.proc and (' ' + self.proc)))
164 self.proc and (' ' + self.proc)))
165 fp.write(''.join(self.before + self.hunk + self.after))
165 fp.write(''.join(self.before + self.hunk + self.after))
166
166
167 pretty = write
167 pretty = write
168
168
169 def filename(self):
169 def filename(self):
170 return self.header.filename()
170 return self.header.filename()
171
171
172 def __repr__(self):
172 def __repr__(self):
173 return '<hunk %r@%d>' % (self.filename(), self.fromline)
173 return '<hunk %r@%d>' % (self.filename(), self.fromline)
174
174
175 def parsepatch(fp):
175 def parsepatch(fp):
176 """patch -> [] of hunks """
176 """patch -> [] of hunks """
177 class parser(object):
177 class parser(object):
178 """patch parsing state machine"""
178 """patch parsing state machine"""
179 def __init__(self):
179 def __init__(self):
180 self.fromline = 0
180 self.fromline = 0
181 self.toline = 0
181 self.toline = 0
182 self.proc = ''
182 self.proc = ''
183 self.header = None
183 self.header = None
184 self.context = []
184 self.context = []
185 self.before = []
185 self.before = []
186 self.hunk = []
186 self.hunk = []
187 self.stream = []
187 self.stream = []
188
188
189 def addrange(self, (fromstart, fromend, tostart, toend, proc)):
189 def addrange(self, (fromstart, fromend, tostart, toend, proc)):
190 self.fromline = int(fromstart)
190 self.fromline = int(fromstart)
191 self.toline = int(tostart)
191 self.toline = int(tostart)
192 self.proc = proc
192 self.proc = proc
193
193
194 def addcontext(self, context):
194 def addcontext(self, context):
195 if self.hunk:
195 if self.hunk:
196 h = hunk(self.header, self.fromline, self.toline, self.proc,
196 h = hunk(self.header, self.fromline, self.toline, self.proc,
197 self.before, self.hunk, context)
197 self.before, self.hunk, context)
198 self.header.hunks.append(h)
198 self.header.hunks.append(h)
199 self.stream.append(h)
199 self.stream.append(h)
200 self.fromline += len(self.before) + h.removed
200 self.fromline += len(self.before) + h.removed
201 self.toline += len(self.before) + h.added
201 self.toline += len(self.before) + h.added
202 self.before = []
202 self.before = []
203 self.hunk = []
203 self.hunk = []
204 self.proc = ''
204 self.proc = ''
205 self.context = context
205 self.context = context
206
206
207 def addhunk(self, hunk):
207 def addhunk(self, hunk):
208 if self.context:
208 if self.context:
209 self.before = self.context
209 self.before = self.context
210 self.context = []
210 self.context = []
211 self.hunk = hunk
211 self.hunk = hunk
212
212
213 def newfile(self, hdr):
213 def newfile(self, hdr):
214 self.addcontext([])
214 self.addcontext([])
215 h = header(hdr)
215 h = header(hdr)
216 self.stream.append(h)
216 self.stream.append(h)
217 self.header = h
217 self.header = h
218
218
219 def finished(self):
219 def finished(self):
220 self.addcontext([])
220 self.addcontext([])
221 return self.stream
221 return self.stream
222
222
223 transitions = {
223 transitions = {
224 'file': {'context': addcontext,
224 'file': {'context': addcontext,
225 'file': newfile,
225 'file': newfile,
226 'hunk': addhunk,
226 'hunk': addhunk,
227 'range': addrange},
227 'range': addrange},
228 'context': {'file': newfile,
228 'context': {'file': newfile,
229 'hunk': addhunk,
229 'hunk': addhunk,
230 'range': addrange},
230 'range': addrange},
231 'hunk': {'context': addcontext,
231 'hunk': {'context': addcontext,
232 'file': newfile,
232 'file': newfile,
233 'range': addrange},
233 'range': addrange},
234 'range': {'context': addcontext,
234 'range': {'context': addcontext,
235 'hunk': addhunk},
235 'hunk': addhunk},
236 }
236 }
237
237
238 p = parser()
238 p = parser()
239
239
240 state = 'context'
240 state = 'context'
241 for newstate, data in scanpatch(fp):
241 for newstate, data in scanpatch(fp):
242 try:
242 try:
243 p.transitions[state][newstate](p, data)
243 p.transitions[state][newstate](p, data)
244 except KeyError:
244 except KeyError:
245 raise patch.PatchError('unhandled transition: %s -> %s' %
245 raise patch.PatchError('unhandled transition: %s -> %s' %
246 (state, newstate))
246 (state, newstate))
247 state = newstate
247 state = newstate
248 return p.finished()
248 return p.finished()
249
249
250 def filterpatch(ui, chunks):
250 def filterpatch(ui, chunks):
251 """Interactively filter patch chunks into applied-only chunks"""
251 """Interactively filter patch chunks into applied-only chunks"""
252 chunks = list(chunks)
252 chunks = list(chunks)
253 chunks.reverse()
253 chunks.reverse()
254 seen = {}
254 seen = {}
255 def consumefile():
255 def consumefile():
256 """fetch next portion from chunks until a 'header' is seen
256 """fetch next portion from chunks until a 'header' is seen
257 NB: header == new-file mark
257 NB: header == new-file mark
258 """
258 """
259 consumed = []
259 consumed = []
260 while chunks:
260 while chunks:
261 if isinstance(chunks[-1], header):
261 if isinstance(chunks[-1], header):
262 break
262 break
263 else:
263 else:
264 consumed.append(chunks.pop())
264 consumed.append(chunks.pop())
265 return consumed
265 return consumed
266
266
267 resp_all = [None] # this two are changed from inside prompt,
267 resp_all = [None] # this two are changed from inside prompt,
268 resp_file = [None] # so can't be usual variables
268 resp_file = [None] # so can't be usual variables
269 applied = {} # 'filename' -> [] of chunks
269 applied = {} # 'filename' -> [] of chunks
270 def prompt(query):
270 def prompt(query):
271 """prompt query, and process base inputs
271 """prompt query, and process base inputs
272
272
273 - y/n for the rest of file
273 - y/n for the rest of file
274 - y/n for the rest
274 - y/n for the rest
275 - ? (help)
275 - ? (help)
276 - q (quit)
276 - q (quit)
277
277
278 else, input is returned to the caller.
278 else, input is returned to the caller.
279 """
279 """
280 if resp_all[0] is not None:
280 if resp_all[0] is not None:
281 return resp_all[0]
281 return resp_all[0]
282 if resp_file[0] is not None:
282 if resp_file[0] is not None:
283 return resp_file[0]
283 return resp_file[0]
284 while True:
284 while True:
285 resps = _('[Ynsfdaq?]')
285 resps = _('[Ynsfdaq?]')
286 choices = (_('&Yes, record this change'),
286 choices = (_('&Yes, record this change'),
287 _('&No, skip this change'),
287 _('&No, skip this change'),
288 _('&Skip remaining changes to this file'),
288 _('&Skip remaining changes to this file'),
289 _('Record remaining changes to this &file'),
289 _('Record remaining changes to this &file'),
290 _('&Done, skip remaining changes and files'),
290 _('&Done, skip remaining changes and files'),
291 _('Record &all changes to all remaining files'),
291 _('Record &all changes to all remaining files'),
292 _('&Quit, recording no changes'),
292 _('&Quit, recording no changes'),
293 _('&?'))
293 _('&?'))
294 r = (ui.prompt("%s %s " % (query, resps), choices)
294 r = (ui.prompt("%s %s " % (query, resps), choices)
295 or _('y')).lower()
295 or _('y')).lower()
296 if r == _('?'):
296 if r == _('?'):
297 doc = gettext(record.__doc__)
297 doc = gettext(record.__doc__)
298 c = doc.find(_('y - record this change'))
298 c = doc.find(_('y - record this change'))
299 for l in doc[c:].splitlines():
299 for l in doc[c:].splitlines():
300 if l: ui.write(l.strip(), '\n')
300 if l: ui.write(l.strip(), '\n')
301 continue
301 continue
302 elif r == _('s'):
302 elif r == _('s'):
303 r = resp_file[0] = 'n'
303 r = resp_file[0] = 'n'
304 elif r == _('f'):
304 elif r == _('f'):
305 r = resp_file[0] = 'y'
305 r = resp_file[0] = 'y'
306 elif r == _('d'):
306 elif r == _('d'):
307 r = resp_all[0] = 'n'
307 r = resp_all[0] = 'n'
308 elif r == _('a'):
308 elif r == _('a'):
309 r = resp_all[0] = 'y'
309 r = resp_all[0] = 'y'
310 elif r == _('q'):
310 elif r == _('q'):
311 raise util.Abort(_('user quit'))
311 raise util.Abort(_('user quit'))
312 return r
312 return r
313 pos, total = 0, len(chunks) - 1
313 pos, total = 0, len(chunks) - 1
314 while chunks:
314 while chunks:
315 chunk = chunks.pop()
315 chunk = chunks.pop()
316 if isinstance(chunk, header):
316 if isinstance(chunk, header):
317 # new-file mark
317 # new-file mark
318 resp_file = [None]
318 resp_file = [None]
319 fixoffset = 0
319 fixoffset = 0
320 hdr = ''.join(chunk.header)
320 hdr = ''.join(chunk.header)
321 if hdr in seen:
321 if hdr in seen:
322 consumefile()
322 consumefile()
323 continue
323 continue
324 seen[hdr] = True
324 seen[hdr] = True
325 if resp_all[0] is None:
325 if resp_all[0] is None:
326 chunk.pretty(ui)
326 chunk.pretty(ui)
327 r = prompt(_('examine changes to %s?') %
327 r = prompt(_('examine changes to %s?') %
328 _(' and ').join(map(repr, chunk.files())))
328 _(' and ').join(map(repr, chunk.files())))
329 if r == _('y'):
329 if r == _('y'):
330 applied[chunk.filename()] = [chunk]
330 applied[chunk.filename()] = [chunk]
331 if chunk.allhunks():
331 if chunk.allhunks():
332 applied[chunk.filename()] += consumefile()
332 applied[chunk.filename()] += consumefile()
333 else:
333 else:
334 consumefile()
334 consumefile()
335 else:
335 else:
336 # new hunk
336 # new hunk
337 if resp_file[0] is None and resp_all[0] is None:
337 if resp_file[0] is None and resp_all[0] is None:
338 chunk.pretty(ui)
338 chunk.pretty(ui)
339 r = total == 1 and prompt(_('record this change to %r?') %
339 r = total == 1 and prompt(_('record this change to %r?') %
340 chunk.filename()) \
340 chunk.filename()) \
341 or prompt(_('record change %d/%d to %r?') %
341 or prompt(_('record change %d/%d to %r?') %
342 (pos, total, chunk.filename()))
342 (pos, total, chunk.filename()))
343 if r == _('y'):
343 if r == _('y'):
344 if fixoffset:
344 if fixoffset:
345 chunk = copy.copy(chunk)
345 chunk = copy.copy(chunk)
346 chunk.toline += fixoffset
346 chunk.toline += fixoffset
347 applied[chunk.filename()].append(chunk)
347 applied[chunk.filename()].append(chunk)
348 else:
348 else:
349 fixoffset += chunk.removed - chunk.added
349 fixoffset += chunk.removed - chunk.added
350 pos = pos + 1
350 pos = pos + 1
351 return reduce(operator.add, [h for h in applied.itervalues()
351 return reduce(operator.add, [h for h in applied.itervalues()
352 if h[0].special() or len(h) > 1], [])
352 if h[0].special() or len(h) > 1], [])
353
353
354 def record(ui, repo, *pats, **opts):
354 def record(ui, repo, *pats, **opts):
355 '''interactively select changes to commit
355 '''interactively select changes to commit
356
356
357 If a list of files is omitted, all changes reported by "hg status"
357 If a list of files is omitted, all changes reported by "hg status"
358 will be candidates for recording.
358 will be candidates for recording.
359
359
360 See 'hg help dates' for a list of formats valid for -d/--date.
360 See 'hg help dates' for a list of formats valid for -d/--date.
361
361
362 You will be prompted for whether to record changes to each
362 You will be prompted for whether to record changes to each
363 modified file, and for files with multiple changes, for each
363 modified file, and for files with multiple changes, for each
364 change to use. For each query, the following responses are
364 change to use. For each query, the following responses are
365 possible:
365 possible:
366
366
367 y - record this change
367 y - record this change
368 n - skip this change
368 n - skip this change
369
369
370 s - skip remaining changes to this file
370 s - skip remaining changes to this file
371 f - record remaining changes to this file
371 f - record remaining changes to this file
372
372
373 d - done, skip remaining changes and files
373 d - done, skip remaining changes and files
374 a - record all changes to all remaining files
374 a - record all changes to all remaining files
375 q - quit, recording no changes
375 q - quit, recording no changes
376
376
377 ? - display help'''
377 ? - display help'''
378
378
379 def record_committer(ui, repo, pats, opts):
379 def record_committer(ui, repo, pats, opts):
380 commands.commit(ui, repo, *pats, **opts)
380 commands.commit(ui, repo, *pats, **opts)
381
381
382 dorecord(ui, repo, record_committer, *pats, **opts)
382 dorecord(ui, repo, record_committer, *pats, **opts)
383
383
384
384
385 def qrecord(ui, repo, patch, *pats, **opts):
385 def qrecord(ui, repo, patch, *pats, **opts):
386 '''interactively record a new patch
386 '''interactively record a new patch
387
387
388 see 'hg help qnew' & 'hg help record' for more information and usage
388 See 'hg help qnew' & 'hg help record' for more information and
389 usage.
389 '''
390 '''
390
391
391 try:
392 try:
392 mq = extensions.find('mq')
393 mq = extensions.find('mq')
393 except KeyError:
394 except KeyError:
394 raise util.Abort(_("'mq' extension not loaded"))
395 raise util.Abort(_("'mq' extension not loaded"))
395
396
396 def qrecord_committer(ui, repo, pats, opts):
397 def qrecord_committer(ui, repo, pats, opts):
397 mq.new(ui, repo, patch, *pats, **opts)
398 mq.new(ui, repo, patch, *pats, **opts)
398
399
399 opts = opts.copy()
400 opts = opts.copy()
400 opts['force'] = True # always 'qnew -f'
401 opts['force'] = True # always 'qnew -f'
401 dorecord(ui, repo, qrecord_committer, *pats, **opts)
402 dorecord(ui, repo, qrecord_committer, *pats, **opts)
402
403
403
404
404 def dorecord(ui, repo, committer, *pats, **opts):
405 def dorecord(ui, repo, committer, *pats, **opts):
405 if not ui.interactive():
406 if not ui.interactive():
406 raise util.Abort(_('running non-interactively, use commit instead'))
407 raise util.Abort(_('running non-interactively, use commit instead'))
407
408
408 def recordfunc(ui, repo, message, match, opts):
409 def recordfunc(ui, repo, message, match, opts):
409 """This is generic record driver.
410 """This is generic record driver.
410
411
411 It's job is to interactively filter local changes, and accordingly
412 It's job is to interactively filter local changes, and accordingly
412 prepare working dir into a state, where the job can be delegated to
413 prepare working dir into a state, where the job can be delegated to
413 non-interactive commit command such as 'commit' or 'qrefresh'.
414 non-interactive commit command such as 'commit' or 'qrefresh'.
414
415
415 After the actual job is done by non-interactive command, working dir
416 After the actual job is done by non-interactive command, working dir
416 state is restored to original.
417 state is restored to original.
417
418
418 In the end we'll record intresting changes, and everything else will be
419 In the end we'll record intresting changes, and everything else will be
419 left in place, so the user can continue his work.
420 left in place, so the user can continue his work.
420 """
421 """
421
422
422 changes = repo.status(match=match)[:3]
423 changes = repo.status(match=match)[:3]
423 diffopts = mdiff.diffopts(git=True, nodates=True)
424 diffopts = mdiff.diffopts(git=True, nodates=True)
424 chunks = patch.diff(repo, changes=changes, opts=diffopts)
425 chunks = patch.diff(repo, changes=changes, opts=diffopts)
425 fp = cStringIO.StringIO()
426 fp = cStringIO.StringIO()
426 fp.write(''.join(chunks))
427 fp.write(''.join(chunks))
427 fp.seek(0)
428 fp.seek(0)
428
429
429 # 1. filter patch, so we have intending-to apply subset of it
430 # 1. filter patch, so we have intending-to apply subset of it
430 chunks = filterpatch(ui, parsepatch(fp))
431 chunks = filterpatch(ui, parsepatch(fp))
431 del fp
432 del fp
432
433
433 contenders = set()
434 contenders = set()
434 for h in chunks:
435 for h in chunks:
435 try: contenders.update(set(h.files()))
436 try: contenders.update(set(h.files()))
436 except AttributeError: pass
437 except AttributeError: pass
437
438
438 changed = changes[0] + changes[1] + changes[2]
439 changed = changes[0] + changes[1] + changes[2]
439 newfiles = [f for f in changed if f in contenders]
440 newfiles = [f for f in changed if f in contenders]
440 if not newfiles:
441 if not newfiles:
441 ui.status(_('no changes to record\n'))
442 ui.status(_('no changes to record\n'))
442 return 0
443 return 0
443
444
444 modified = set(changes[0])
445 modified = set(changes[0])
445
446
446 # 2. backup changed files, so we can restore them in the end
447 # 2. backup changed files, so we can restore them in the end
447 backups = {}
448 backups = {}
448 backupdir = repo.join('record-backups')
449 backupdir = repo.join('record-backups')
449 try:
450 try:
450 os.mkdir(backupdir)
451 os.mkdir(backupdir)
451 except OSError, err:
452 except OSError, err:
452 if err.errno != errno.EEXIST:
453 if err.errno != errno.EEXIST:
453 raise
454 raise
454 try:
455 try:
455 # backup continues
456 # backup continues
456 for f in newfiles:
457 for f in newfiles:
457 if f not in modified:
458 if f not in modified:
458 continue
459 continue
459 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
460 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
460 dir=backupdir)
461 dir=backupdir)
461 os.close(fd)
462 os.close(fd)
462 ui.debug(_('backup %r as %r\n') % (f, tmpname))
463 ui.debug(_('backup %r as %r\n') % (f, tmpname))
463 util.copyfile(repo.wjoin(f), tmpname)
464 util.copyfile(repo.wjoin(f), tmpname)
464 backups[f] = tmpname
465 backups[f] = tmpname
465
466
466 fp = cStringIO.StringIO()
467 fp = cStringIO.StringIO()
467 for c in chunks:
468 for c in chunks:
468 if c.filename() in backups:
469 if c.filename() in backups:
469 c.write(fp)
470 c.write(fp)
470 dopatch = fp.tell()
471 dopatch = fp.tell()
471 fp.seek(0)
472 fp.seek(0)
472
473
473 # 3a. apply filtered patch to clean repo (clean)
474 # 3a. apply filtered patch to clean repo (clean)
474 if backups:
475 if backups:
475 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
476 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
476
477
477 # 3b. (apply)
478 # 3b. (apply)
478 if dopatch:
479 if dopatch:
479 try:
480 try:
480 ui.debug(_('applying patch\n'))
481 ui.debug(_('applying patch\n'))
481 ui.debug(fp.getvalue())
482 ui.debug(fp.getvalue())
482 pfiles = {}
483 pfiles = {}
483 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles)
484 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles)
484 patch.updatedir(ui, repo, pfiles)
485 patch.updatedir(ui, repo, pfiles)
485 except patch.PatchError, err:
486 except patch.PatchError, err:
486 s = str(err)
487 s = str(err)
487 if s:
488 if s:
488 raise util.Abort(s)
489 raise util.Abort(s)
489 else:
490 else:
490 raise util.Abort(_('patch failed to apply'))
491 raise util.Abort(_('patch failed to apply'))
491 del fp
492 del fp
492
493
493 # 4. We prepared working directory according to filtered patch.
494 # 4. We prepared working directory according to filtered patch.
494 # Now is the time to delegate the job to commit/qrefresh or the like!
495 # Now is the time to delegate the job to commit/qrefresh or the like!
495
496
496 # it is important to first chdir to repo root -- we'll call a
497 # it is important to first chdir to repo root -- we'll call a
497 # highlevel command with list of pathnames relative to repo root
498 # highlevel command with list of pathnames relative to repo root
498 cwd = os.getcwd()
499 cwd = os.getcwd()
499 os.chdir(repo.root)
500 os.chdir(repo.root)
500 try:
501 try:
501 committer(ui, repo, newfiles, opts)
502 committer(ui, repo, newfiles, opts)
502 finally:
503 finally:
503 os.chdir(cwd)
504 os.chdir(cwd)
504
505
505 return 0
506 return 0
506 finally:
507 finally:
507 # 5. finally restore backed-up files
508 # 5. finally restore backed-up files
508 try:
509 try:
509 for realname, tmpname in backups.iteritems():
510 for realname, tmpname in backups.iteritems():
510 ui.debug(_('restoring %r to %r\n') % (tmpname, realname))
511 ui.debug(_('restoring %r to %r\n') % (tmpname, realname))
511 util.copyfile(tmpname, repo.wjoin(realname))
512 util.copyfile(tmpname, repo.wjoin(realname))
512 os.unlink(tmpname)
513 os.unlink(tmpname)
513 os.rmdir(backupdir)
514 os.rmdir(backupdir)
514 except OSError:
515 except OSError:
515 pass
516 pass
516 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
517 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
517
518
518 cmdtable = {
519 cmdtable = {
519 "record":
520 "record":
520 (record,
521 (record,
521
522
522 # add commit options
523 # add commit options
523 commands.table['^commit|ci'][1],
524 commands.table['^commit|ci'][1],
524
525
525 _('hg record [OPTION]... [FILE]...')),
526 _('hg record [OPTION]... [FILE]...')),
526 }
527 }
527
528
528
529
529 def extsetup():
530 def extsetup():
530 try:
531 try:
531 mq = extensions.find('mq')
532 mq = extensions.find('mq')
532 except KeyError:
533 except KeyError:
533 return
534 return
534
535
535 qcmdtable = {
536 qcmdtable = {
536 "qrecord":
537 "qrecord":
537 (qrecord,
538 (qrecord,
538
539
539 # add qnew options, except '--force'
540 # add qnew options, except '--force'
540 [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'],
541 [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'],
541
542
542 _('hg qrecord [OPTION]... PATCH [FILE]...')),
543 _('hg qrecord [OPTION]... PATCH [FILE]...')),
543 }
544 }
544
545
545 cmdtable.update(qcmdtable)
546 cmdtable.update(qcmdtable)
546
547
@@ -1,207 +1,208 b''
1 % help (no mq, so no qrecord)
1 % help (no mq, so no qrecord)
2 hg: unknown command 'qrecord'
2 hg: unknown command 'qrecord'
3 Mercurial Distributed SCM
3 Mercurial Distributed SCM
4
4
5 basic commands:
5 basic commands:
6
6
7 add add the specified files on the next commit
7 add add the specified files on the next commit
8 annotate show changeset information per file line
8 annotate show changeset information per file line
9 clone make a copy of an existing repository
9 clone make a copy of an existing repository
10 commit commit the specified files or all outstanding changes
10 commit commit the specified files or all outstanding changes
11 diff diff repository (or selected files)
11 diff diff repository (or selected files)
12 export dump the header and diffs for one or more changesets
12 export dump the header and diffs for one or more changesets
13 init create a new repository in the given directory
13 init create a new repository in the given directory
14 log show revision history of entire repository or files
14 log show revision history of entire repository or files
15 merge merge working directory with another revision
15 merge merge working directory with another revision
16 parents show the parents of the working directory or revision
16 parents show the parents of the working directory or revision
17 pull pull changes from the specified source
17 pull pull changes from the specified source
18 push push changes to the specified destination
18 push push changes to the specified destination
19 remove remove the specified files on the next commit
19 remove remove the specified files on the next commit
20 serve export the repository via HTTP
20 serve export the repository via HTTP
21 status show changed files in the working directory
21 status show changed files in the working directory
22 update update working directory
22 update update working directory
23
23
24 use "hg help" for the full list of commands or "hg -v" for details
24 use "hg help" for the full list of commands or "hg -v" for details
25 % help (mq present)
25 % help (mq present)
26 hg qrecord [OPTION]... PATCH [FILE]...
26 hg qrecord [OPTION]... PATCH [FILE]...
27
27
28 interactively record a new patch
28 interactively record a new patch
29
29
30 see 'hg help qnew' & 'hg help record' for more information and usage
30 See 'hg help qnew' & 'hg help record' for more information and
31 usage.
31
32
32 options:
33 options:
33
34
34 -e --edit edit commit message
35 -e --edit edit commit message
35 -g --git use git extended diff format
36 -g --git use git extended diff format
36 -U --currentuser add "From: <current user>" to patch
37 -U --currentuser add "From: <current user>" to patch
37 -u --user add "From: <given user>" to patch
38 -u --user add "From: <given user>" to patch
38 -D --currentdate add "Date: <current date>" to patch
39 -D --currentdate add "Date: <current date>" to patch
39 -d --date add "Date: <given date>" to patch
40 -d --date add "Date: <given date>" to patch
40 -I --include include names matching the given patterns
41 -I --include include names matching the given patterns
41 -X --exclude exclude names matching the given patterns
42 -X --exclude exclude names matching the given patterns
42 -m --message use <text> as commit message
43 -m --message use <text> as commit message
43 -l --logfile read commit message from <file>
44 -l --logfile read commit message from <file>
44
45
45 use "hg -v help qrecord" to show global options
46 use "hg -v help qrecord" to show global options
46 % base commit
47 % base commit
47 % changing files
48 % changing files
48 % whole diff
49 % whole diff
49 diff -r 1057167b20ef 1.txt
50 diff -r 1057167b20ef 1.txt
50 --- a/1.txt
51 --- a/1.txt
51 +++ b/1.txt
52 +++ b/1.txt
52 @@ -1,5 +1,5 @@
53 @@ -1,5 +1,5 @@
53 1
54 1
54 -2
55 -2
55 +2 2
56 +2 2
56 3
57 3
57 -4
58 -4
58 +4 4
59 +4 4
59 5
60 5
60 diff -r 1057167b20ef 2.txt
61 diff -r 1057167b20ef 2.txt
61 --- a/2.txt
62 --- a/2.txt
62 +++ b/2.txt
63 +++ b/2.txt
63 @@ -1,5 +1,5 @@
64 @@ -1,5 +1,5 @@
64 a
65 a
65 -b
66 -b
66 +b b
67 +b b
67 c
68 c
68 d
69 d
69 e
70 e
70 diff -r 1057167b20ef dir/a.txt
71 diff -r 1057167b20ef dir/a.txt
71 --- a/dir/a.txt
72 --- a/dir/a.txt
72 +++ b/dir/a.txt
73 +++ b/dir/a.txt
73 @@ -1,4 +1,4 @@
74 @@ -1,4 +1,4 @@
74 -hello world
75 -hello world
75 +hello world!
76 +hello world!
76
77
77 someone
78 someone
78 up
79 up
79 % qrecord a.patch
80 % qrecord a.patch
80 diff --git a/1.txt b/1.txt
81 diff --git a/1.txt b/1.txt
81 2 hunks, 4 lines changed
82 2 hunks, 4 lines changed
82 examine changes to '1.txt'? [Ynsfdaq?] @@ -1,3 +1,3 @@
83 examine changes to '1.txt'? [Ynsfdaq?] @@ -1,3 +1,3 @@
83 1
84 1
84 -2
85 -2
85 +2 2
86 +2 2
86 3
87 3
87 record change 1/6 to '1.txt'? [Ynsfdaq?] @@ -3,3 +3,3 @@
88 record change 1/6 to '1.txt'? [Ynsfdaq?] @@ -3,3 +3,3 @@
88 3
89 3
89 -4
90 -4
90 +4 4
91 +4 4
91 5
92 5
92 record change 2/6 to '1.txt'? [Ynsfdaq?] diff --git a/2.txt b/2.txt
93 record change 2/6 to '1.txt'? [Ynsfdaq?] diff --git a/2.txt b/2.txt
93 1 hunks, 2 lines changed
94 1 hunks, 2 lines changed
94 examine changes to '2.txt'? [Ynsfdaq?] @@ -1,5 +1,5 @@
95 examine changes to '2.txt'? [Ynsfdaq?] @@ -1,5 +1,5 @@
95 a
96 a
96 -b
97 -b
97 +b b
98 +b b
98 c
99 c
99 d
100 d
100 e
101 e
101 record change 4/6 to '2.txt'? [Ynsfdaq?] diff --git a/dir/a.txt b/dir/a.txt
102 record change 4/6 to '2.txt'? [Ynsfdaq?] diff --git a/dir/a.txt b/dir/a.txt
102 1 hunks, 2 lines changed
103 1 hunks, 2 lines changed
103 examine changes to 'dir/a.txt'? [Ynsfdaq?]
104 examine changes to 'dir/a.txt'? [Ynsfdaq?]
104 % after qrecord a.patch 'tip'
105 % after qrecord a.patch 'tip'
105 changeset: 1:5d1ca63427ee
106 changeset: 1:5d1ca63427ee
106 tag: qtip
107 tag: qtip
107 tag: tip
108 tag: tip
108 tag: a.patch
109 tag: a.patch
109 tag: qbase
110 tag: qbase
110 user: test
111 user: test
111 date: Thu Jan 01 00:00:00 1970 +0000
112 date: Thu Jan 01 00:00:00 1970 +0000
112 summary: aaa
113 summary: aaa
113
114
114 diff -r 1057167b20ef -r 5d1ca63427ee 1.txt
115 diff -r 1057167b20ef -r 5d1ca63427ee 1.txt
115 --- a/1.txt Thu Jan 01 00:00:00 1970 +0000
116 --- a/1.txt Thu Jan 01 00:00:00 1970 +0000
116 +++ b/1.txt Thu Jan 01 00:00:00 1970 +0000
117 +++ b/1.txt Thu Jan 01 00:00:00 1970 +0000
117 @@ -1,5 +1,5 @@
118 @@ -1,5 +1,5 @@
118 1
119 1
119 -2
120 -2
120 +2 2
121 +2 2
121 3
122 3
122 4
123 4
123 5
124 5
124 diff -r 1057167b20ef -r 5d1ca63427ee 2.txt
125 diff -r 1057167b20ef -r 5d1ca63427ee 2.txt
125 --- a/2.txt Thu Jan 01 00:00:00 1970 +0000
126 --- a/2.txt Thu Jan 01 00:00:00 1970 +0000
126 +++ b/2.txt Thu Jan 01 00:00:00 1970 +0000
127 +++ b/2.txt Thu Jan 01 00:00:00 1970 +0000
127 @@ -1,5 +1,5 @@
128 @@ -1,5 +1,5 @@
128 a
129 a
129 -b
130 -b
130 +b b
131 +b b
131 c
132 c
132 d
133 d
133 e
134 e
134
135
135
136
136 % after qrecord a.patch 'diff'
137 % after qrecord a.patch 'diff'
137 diff -r 5d1ca63427ee 1.txt
138 diff -r 5d1ca63427ee 1.txt
138 --- a/1.txt
139 --- a/1.txt
139 +++ b/1.txt
140 +++ b/1.txt
140 @@ -1,5 +1,5 @@
141 @@ -1,5 +1,5 @@
141 1
142 1
142 2 2
143 2 2
143 3
144 3
144 -4
145 -4
145 +4 4
146 +4 4
146 5
147 5
147 diff -r 5d1ca63427ee dir/a.txt
148 diff -r 5d1ca63427ee dir/a.txt
148 --- a/dir/a.txt
149 --- a/dir/a.txt
149 +++ b/dir/a.txt
150 +++ b/dir/a.txt
150 @@ -1,4 +1,4 @@
151 @@ -1,4 +1,4 @@
151 -hello world
152 -hello world
152 +hello world!
153 +hello world!
153
154
154 someone
155 someone
155 up
156 up
156 % qrecord b.patch
157 % qrecord b.patch
157 diff --git a/1.txt b/1.txt
158 diff --git a/1.txt b/1.txt
158 1 hunks, 2 lines changed
159 1 hunks, 2 lines changed
159 examine changes to '1.txt'? [Ynsfdaq?] @@ -1,5 +1,5 @@
160 examine changes to '1.txt'? [Ynsfdaq?] @@ -1,5 +1,5 @@
160 1
161 1
161 2 2
162 2 2
162 3
163 3
163 -4
164 -4
164 +4 4
165 +4 4
165 5
166 5
166 record change 1/3 to '1.txt'? [Ynsfdaq?] diff --git a/dir/a.txt b/dir/a.txt
167 record change 1/3 to '1.txt'? [Ynsfdaq?] diff --git a/dir/a.txt b/dir/a.txt
167 1 hunks, 2 lines changed
168 1 hunks, 2 lines changed
168 examine changes to 'dir/a.txt'? [Ynsfdaq?] @@ -1,4 +1,4 @@
169 examine changes to 'dir/a.txt'? [Ynsfdaq?] @@ -1,4 +1,4 @@
169 -hello world
170 -hello world
170 +hello world!
171 +hello world!
171
172
172 someone
173 someone
173 up
174 up
174 record change 3/3 to 'dir/a.txt'? [Ynsfdaq?]
175 record change 3/3 to 'dir/a.txt'? [Ynsfdaq?]
175 % after qrecord b.patch 'tip'
176 % after qrecord b.patch 'tip'
176 changeset: 2:b056198bf878
177 changeset: 2:b056198bf878
177 tag: qtip
178 tag: qtip
178 tag: tip
179 tag: tip
179 tag: b.patch
180 tag: b.patch
180 user: test
181 user: test
181 date: Thu Jan 01 00:00:00 1970 +0000
182 date: Thu Jan 01 00:00:00 1970 +0000
182 summary: bbb
183 summary: bbb
183
184
184 diff -r 5d1ca63427ee -r b056198bf878 1.txt
185 diff -r 5d1ca63427ee -r b056198bf878 1.txt
185 --- a/1.txt Thu Jan 01 00:00:00 1970 +0000
186 --- a/1.txt Thu Jan 01 00:00:00 1970 +0000
186 +++ b/1.txt Thu Jan 01 00:00:00 1970 +0000
187 +++ b/1.txt Thu Jan 01 00:00:00 1970 +0000
187 @@ -1,5 +1,5 @@
188 @@ -1,5 +1,5 @@
188 1
189 1
189 2 2
190 2 2
190 3
191 3
191 -4
192 -4
192 +4 4
193 +4 4
193 5
194 5
194 diff -r 5d1ca63427ee -r b056198bf878 dir/a.txt
195 diff -r 5d1ca63427ee -r b056198bf878 dir/a.txt
195 --- a/dir/a.txt Thu Jan 01 00:00:00 1970 +0000
196 --- a/dir/a.txt Thu Jan 01 00:00:00 1970 +0000
196 +++ b/dir/a.txt Thu Jan 01 00:00:00 1970 +0000
197 +++ b/dir/a.txt Thu Jan 01 00:00:00 1970 +0000
197 @@ -1,4 +1,4 @@
198 @@ -1,4 +1,4 @@
198 -hello world
199 -hello world
199 +hello world!
200 +hello world!
200
201
201 someone
202 someone
202 up
203 up
203
204
204
205
205 % after qrecord b.patch 'diff'
206 % after qrecord b.patch 'diff'
206
207
207 % --- end ---
208 % --- end ---
General Comments 0
You need to be logged in to leave comments. Login now