##// END OF EJS Templates
coding style: fix gratuitous whitespace after Python keywords
Thomas Arendsen Hein -
r13075:d73c3034 default
parent child Browse files
Show More
@@ -1,569 +1,569 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, 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([max(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, limits):
190 190 fromstart, fromend, tostart, toend, proc = limits
191 191 self.fromline = int(fromstart)
192 192 self.toline = int(tostart)
193 193 self.proc = proc
194 194
195 195 def addcontext(self, context):
196 196 if self.hunk:
197 197 h = hunk(self.header, self.fromline, self.toline, self.proc,
198 198 self.before, self.hunk, context)
199 199 self.header.hunks.append(h)
200 200 self.stream.append(h)
201 201 self.fromline += len(self.before) + h.removed
202 202 self.toline += len(self.before) + h.added
203 203 self.before = []
204 204 self.hunk = []
205 205 self.proc = ''
206 206 self.context = context
207 207
208 208 def addhunk(self, hunk):
209 209 if self.context:
210 210 self.before = self.context
211 211 self.context = []
212 212 self.hunk = hunk
213 213
214 214 def newfile(self, hdr):
215 215 self.addcontext([])
216 216 h = header(hdr)
217 217 self.stream.append(h)
218 218 self.header = h
219 219
220 220 def finished(self):
221 221 self.addcontext([])
222 222 return self.stream
223 223
224 224 transitions = {
225 225 'file': {'context': addcontext,
226 226 'file': newfile,
227 227 'hunk': addhunk,
228 228 'range': addrange},
229 229 'context': {'file': newfile,
230 230 'hunk': addhunk,
231 231 'range': addrange},
232 232 'hunk': {'context': addcontext,
233 233 'file': newfile,
234 234 'range': addrange},
235 235 'range': {'context': addcontext,
236 236 'hunk': addhunk},
237 237 }
238 238
239 239 p = parser()
240 240
241 241 state = 'context'
242 242 for newstate, data in scanpatch(fp):
243 243 try:
244 244 p.transitions[state][newstate](p, data)
245 245 except KeyError:
246 246 raise patch.PatchError('unhandled transition: %s -> %s' %
247 247 (state, newstate))
248 248 state = newstate
249 249 return p.finished()
250 250
251 251 def filterpatch(ui, chunks):
252 252 """Interactively filter patch chunks into applied-only chunks"""
253 253 chunks = list(chunks)
254 254 chunks.reverse()
255 255 seen = set()
256 256 def consumefile():
257 257 """fetch next portion from chunks until a 'header' is seen
258 258 NB: header == new-file mark
259 259 """
260 260 consumed = []
261 261 while chunks:
262 262 if isinstance(chunks[-1], header):
263 263 break
264 264 else:
265 265 consumed.append(chunks.pop())
266 266 return consumed
267 267
268 268 resp_all = [None] # this two are changed from inside prompt,
269 269 resp_file = [None] # so can't be usual variables
270 270 applied = {} # 'filename' -> [] of chunks
271 271 def prompt(query):
272 272 """prompt query, and process base inputs
273 273
274 274 - y/n for the rest of file
275 275 - y/n for the rest
276 276 - ? (help)
277 277 - q (quit)
278 278
279 279 Returns True/False and sets reps_all and resp_file as
280 280 appropriate.
281 281 """
282 282 if resp_all[0] is not None:
283 283 return resp_all[0]
284 284 if resp_file[0] is not None:
285 285 return resp_file[0]
286 286 while True:
287 287 resps = _('[Ynsfdaq?]')
288 288 choices = (_('&Yes, record this change'),
289 289 _('&No, skip this change'),
290 290 _('&Skip remaining changes to this file'),
291 291 _('Record remaining changes to this &file'),
292 292 _('&Done, skip remaining changes and files'),
293 293 _('Record &all changes to all remaining files'),
294 294 _('&Quit, recording no changes'),
295 295 _('&?'))
296 296 r = ui.promptchoice("%s %s" % (query, resps), choices)
297 297 ui.write("\n")
298 298 if r == 7: # ?
299 299 doc = gettext(record.__doc__)
300 300 c = doc.find('::') + 2
301 301 for l in doc[c:].splitlines():
302 302 if l.startswith(' '):
303 303 ui.write(l.strip(), '\n')
304 304 continue
305 305 elif r == 0: # yes
306 306 ret = True
307 307 elif r == 1: # no
308 308 ret = False
309 309 elif r == 2: # Skip
310 310 ret = resp_file[0] = False
311 311 elif r == 3: # file (Record remaining)
312 312 ret = resp_file[0] = True
313 313 elif r == 4: # done, skip remaining
314 314 ret = resp_all[0] = False
315 315 elif r == 5: # all
316 316 ret = resp_all[0] = True
317 317 elif r == 6: # quit
318 318 raise util.Abort(_('user quit'))
319 319 return ret
320 320 pos, total = 0, len(chunks) - 1
321 321 while chunks:
322 322 pos = total - len(chunks) + 1
323 323 chunk = chunks.pop()
324 324 if isinstance(chunk, header):
325 325 # new-file mark
326 326 resp_file = [None]
327 327 fixoffset = 0
328 328 hdr = ''.join(chunk.header)
329 329 if hdr in seen:
330 330 consumefile()
331 331 continue
332 332 seen.add(hdr)
333 333 if resp_all[0] is None:
334 334 chunk.pretty(ui)
335 335 r = prompt(_('examine changes to %s?') %
336 336 _(' and ').join(map(repr, chunk.files())))
337 337 if r:
338 338 applied[chunk.filename()] = [chunk]
339 339 if chunk.allhunks():
340 340 applied[chunk.filename()] += consumefile()
341 341 else:
342 342 consumefile()
343 343 else:
344 344 # new hunk
345 345 if resp_file[0] is None and resp_all[0] is None:
346 346 chunk.pretty(ui)
347 r = total == 1 and prompt(_('record this change to %r?') %
348 chunk.filename()) \
347 r = (total == 1
348 and prompt(_('record this change to %r?') % chunk.filename())
349 349 or prompt(_('record change %d/%d to %r?') %
350 (pos, total, chunk.filename()))
350 (pos, total, chunk.filename())))
351 351 if r:
352 352 if fixoffset:
353 353 chunk = copy.copy(chunk)
354 354 chunk.toline += fixoffset
355 355 applied[chunk.filename()].append(chunk)
356 356 else:
357 357 fixoffset += chunk.removed - chunk.added
358 358 return sum([h for h in applied.itervalues()
359 359 if h[0].special() or len(h) > 1], [])
360 360
361 361 def record(ui, repo, *pats, **opts):
362 362 '''interactively select changes to commit
363 363
364 364 If a list of files is omitted, all changes reported by :hg:`status`
365 365 will be candidates for recording.
366 366
367 367 See :hg:`help dates` for a list of formats valid for -d/--date.
368 368
369 369 You will be prompted for whether to record changes to each
370 370 modified file, and for files with multiple changes, for each
371 371 change to use. For each query, the following responses are
372 372 possible::
373 373
374 374 y - record this change
375 375 n - skip this change
376 376
377 377 s - skip remaining changes to this file
378 378 f - record remaining changes to this file
379 379
380 380 d - done, skip remaining changes and files
381 381 a - record all changes to all remaining files
382 382 q - quit, recording no changes
383 383
384 384 ? - display help
385 385
386 386 This command is not available when committing a merge.'''
387 387
388 388 dorecord(ui, repo, commands.commit, *pats, **opts)
389 389
390 390
391 391 def qrecord(ui, repo, patch, *pats, **opts):
392 392 '''interactively record a new patch
393 393
394 394 See :hg:`help qnew` & :hg:`help record` for more information and
395 395 usage.
396 396 '''
397 397
398 398 try:
399 399 mq = extensions.find('mq')
400 400 except KeyError:
401 401 raise util.Abort(_("'mq' extension not loaded"))
402 402
403 403 def committomq(ui, repo, *pats, **opts):
404 404 mq.new(ui, repo, patch, *pats, **opts)
405 405
406 406 opts = opts.copy()
407 407 opts['force'] = True # always 'qnew -f'
408 408 dorecord(ui, repo, committomq, *pats, **opts)
409 409
410 410
411 411 def dorecord(ui, repo, commitfunc, *pats, **opts):
412 412 if not ui.interactive():
413 413 raise util.Abort(_('running non-interactively, use commit instead'))
414 414
415 415 def recordfunc(ui, repo, message, match, opts):
416 416 """This is generic record driver.
417 417
418 418 Its job is to interactively filter local changes, and accordingly
419 419 prepare working dir into a state, where the job can be delegated to
420 420 non-interactive commit command such as 'commit' or 'qrefresh'.
421 421
422 422 After the actual job is done by non-interactive command, working dir
423 423 state is restored to original.
424 424
425 425 In the end we'll record interesting changes, and everything else will be
426 426 left in place, so the user can continue his work.
427 427 """
428 428
429 429 merge = len(repo[None].parents()) > 1
430 430 if merge:
431 431 raise util.Abort(_('cannot partially commit a merge '
432 432 '(use "hg commit" instead)'))
433 433
434 434 changes = repo.status(match=match)[:3]
435 435 diffopts = mdiff.diffopts(git=True, nodates=True)
436 436 chunks = patch.diff(repo, changes=changes, opts=diffopts)
437 437 fp = cStringIO.StringIO()
438 438 fp.write(''.join(chunks))
439 439 fp.seek(0)
440 440
441 441 # 1. filter patch, so we have intending-to apply subset of it
442 442 chunks = filterpatch(ui, parsepatch(fp))
443 443 del fp
444 444
445 445 contenders = set()
446 446 for h in chunks:
447 447 try:
448 448 contenders.update(set(h.files()))
449 449 except AttributeError:
450 450 pass
451 451
452 452 changed = changes[0] + changes[1] + changes[2]
453 453 newfiles = [f for f in changed if f in contenders]
454 454 if not newfiles:
455 455 ui.status(_('no changes to record\n'))
456 456 return 0
457 457
458 458 modified = set(changes[0])
459 459
460 460 # 2. backup changed files, so we can restore them in the end
461 461 backups = {}
462 462 backupdir = repo.join('record-backups')
463 463 try:
464 464 os.mkdir(backupdir)
465 465 except OSError, err:
466 466 if err.errno != errno.EEXIST:
467 467 raise
468 468 try:
469 469 # backup continues
470 470 for f in newfiles:
471 471 if f not in modified:
472 472 continue
473 473 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
474 474 dir=backupdir)
475 475 os.close(fd)
476 476 ui.debug('backup %r as %r\n' % (f, tmpname))
477 477 util.copyfile(repo.wjoin(f), tmpname)
478 478 backups[f] = tmpname
479 479
480 480 fp = cStringIO.StringIO()
481 481 for c in chunks:
482 482 if c.filename() in backups:
483 483 c.write(fp)
484 484 dopatch = fp.tell()
485 485 fp.seek(0)
486 486
487 487 # 3a. apply filtered patch to clean repo (clean)
488 488 if backups:
489 489 hg.revert(repo, repo.dirstate.parents()[0],
490 490 lambda key: key in backups)
491 491
492 492 # 3b. (apply)
493 493 if dopatch:
494 494 try:
495 495 ui.debug('applying patch\n')
496 496 ui.debug(fp.getvalue())
497 497 pfiles = {}
498 498 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles,
499 499 eolmode=None)
500 500 cmdutil.updatedir(ui, repo, pfiles)
501 501 except patch.PatchError, err:
502 502 raise util.Abort(str(err))
503 503 del fp
504 504
505 505 # 4. We prepared working directory according to filtered patch.
506 506 # Now is the time to delegate the job to commit/qrefresh or the like!
507 507
508 508 # it is important to first chdir to repo root -- we'll call a
509 509 # highlevel command with list of pathnames relative to repo root
510 510 cwd = os.getcwd()
511 511 os.chdir(repo.root)
512 512 try:
513 513 commitfunc(ui, repo, *newfiles, **opts)
514 514 finally:
515 515 os.chdir(cwd)
516 516
517 517 return 0
518 518 finally:
519 519 # 5. finally restore backed-up files
520 520 try:
521 521 for realname, tmpname in backups.iteritems():
522 522 ui.debug('restoring %r to %r\n' % (tmpname, realname))
523 523 util.copyfile(tmpname, repo.wjoin(realname))
524 524 os.unlink(tmpname)
525 525 os.rmdir(backupdir)
526 526 except OSError:
527 527 pass
528 528
529 529 # wrap ui.write so diff output can be labeled/colorized
530 530 def wrapwrite(orig, *args, **kw):
531 531 label = kw.pop('label', '')
532 532 for chunk, l in patch.difflabel(lambda: args):
533 533 orig(chunk, label=label + l)
534 534 oldwrite = ui.write
535 535 extensions.wrapfunction(ui, 'write', wrapwrite)
536 536 try:
537 537 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
538 538 finally:
539 539 ui.write = oldwrite
540 540
541 541 cmdtable = {
542 542 "record":
543 543 (record,
544 544
545 545 # add commit options
546 546 commands.table['^commit|ci'][1],
547 547
548 548 _('hg record [OPTION]... [FILE]...')),
549 549 }
550 550
551 551
552 552 def uisetup(ui):
553 553 try:
554 554 mq = extensions.find('mq')
555 555 except KeyError:
556 556 return
557 557
558 558 qcmdtable = {
559 559 "qrecord":
560 560 (qrecord,
561 561
562 562 # add qnew options, except '--force'
563 563 [opt for opt in mq.cmdtable['^qnew'][1] if opt[1] != 'force'],
564 564
565 565 _('hg qrecord [OPTION]... PATCH [FILE]...')),
566 566 }
567 567
568 568 cmdtable.update(qcmdtable)
569 569
General Comments 0
You need to be logged in to leave comments. Login now