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