##// END OF EJS Templates
record: fix indentation
Benoit Boissinot -
r7911:0b2561b5 default
parent child Browse files
Show More
@@ -1,538 +1,538 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
6 6 # the GNU General Public License, incorporated herein by reference.
7 7
8 8 '''interactive change selection during commit or 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 = {}
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 else, input is returned to the caller.
279 279 """
280 280 if resp_all[0] is not None:
281 281 return resp_all[0]
282 282 if resp_file[0] is not None:
283 283 return resp_file[0]
284 284 while True:
285 285 choices = _('[Ynsfdaq?]')
286 286 r = (ui.prompt("%s %s " % (query, choices), '(?i)%s?$' % choices)
287 287 or _('y')).lower()
288 288 if r == _('?'):
289 289 doc = gettext(record.__doc__)
290 290 c = doc.find(_('y - record this change'))
291 291 for l in doc[c:].splitlines():
292 292 if l: ui.write(l.strip(), '\n')
293 293 continue
294 294 elif r == _('s'):
295 295 r = resp_file[0] = 'n'
296 296 elif r == _('f'):
297 297 r = resp_file[0] = 'y'
298 298 elif r == _('d'):
299 299 r = resp_all[0] = 'n'
300 300 elif r == _('a'):
301 301 r = resp_all[0] = 'y'
302 302 elif r == _('q'):
303 303 raise util.Abort(_('user quit'))
304 304 return r
305 305 pos, total = 0, len(chunks) - 1
306 306 while chunks:
307 307 chunk = chunks.pop()
308 308 if isinstance(chunk, header):
309 309 # new-file mark
310 310 resp_file = [None]
311 311 fixoffset = 0
312 312 hdr = ''.join(chunk.header)
313 313 if hdr in seen:
314 314 consumefile()
315 315 continue
316 316 seen[hdr] = True
317 317 if resp_all[0] is None:
318 318 chunk.pretty(ui)
319 319 r = prompt(_('examine changes to %s?') %
320 320 _(' and ').join(map(repr, chunk.files())))
321 321 if r == _('y'):
322 322 applied[chunk.filename()] = [chunk]
323 323 if chunk.allhunks():
324 324 applied[chunk.filename()] += consumefile()
325 325 else:
326 326 consumefile()
327 327 else:
328 328 # new hunk
329 329 if resp_file[0] is None and resp_all[0] is None:
330 330 chunk.pretty(ui)
331 331 r = total == 1 and prompt(_('record this change to %r?') %
332 chunk.filename()) or \
333 prompt(_('record change %d/%d to %r?') %
334 (pos, total, chunk.filename()))
332 chunk.filename()) \
333 or prompt(_('record change %d/%d to %r?') %
334 (pos, total, chunk.filename()))
335 335 if r == _('y'):
336 336 if fixoffset:
337 337 chunk = copy.copy(chunk)
338 338 chunk.toline += fixoffset
339 339 applied[chunk.filename()].append(chunk)
340 340 else:
341 341 fixoffset += chunk.removed - chunk.added
342 342 pos = pos + 1
343 343 return reduce(operator.add, [h for h in applied.itervalues()
344 344 if h[0].special() or len(h) > 1], [])
345 345
346 346 def record(ui, repo, *pats, **opts):
347 347 '''interactively select changes to commit
348 348
349 349 If a list of files is omitted, all changes reported by "hg status"
350 350 will be candidates for recording.
351 351
352 352 See 'hg help dates' for a list of formats valid for -d/--date.
353 353
354 354 You will be prompted for whether to record changes to each
355 355 modified file, and for files with multiple changes, for each
356 356 change to use. For each query, the following responses are
357 357 possible:
358 358
359 359 y - record this change
360 360 n - skip this change
361 361
362 362 s - skip remaining changes to this file
363 363 f - record remaining changes to this file
364 364
365 365 d - done, skip remaining changes and files
366 366 a - record all changes to all remaining files
367 367 q - quit, recording no changes
368 368
369 369 ? - display help'''
370 370
371 371 def record_committer(ui, repo, pats, opts):
372 372 commands.commit(ui, repo, *pats, **opts)
373 373
374 374 dorecord(ui, repo, record_committer, *pats, **opts)
375 375
376 376
377 377 def qrecord(ui, repo, patch, *pats, **opts):
378 378 '''interactively record a new patch
379 379
380 380 see 'hg help qnew' & 'hg help record' for more information and usage
381 381 '''
382 382
383 383 try:
384 384 mq = extensions.find('mq')
385 385 except KeyError:
386 386 raise util.Abort(_("'mq' extension not loaded"))
387 387
388 388 def qrecord_committer(ui, repo, pats, opts):
389 389 mq.new(ui, repo, patch, *pats, **opts)
390 390
391 391 opts = opts.copy()
392 392 opts['force'] = True # always 'qnew -f'
393 393 dorecord(ui, repo, qrecord_committer, *pats, **opts)
394 394
395 395
396 396 def dorecord(ui, repo, committer, *pats, **opts):
397 397 if not ui.interactive:
398 398 raise util.Abort(_('running non-interactively, use commit instead'))
399 399
400 400 def recordfunc(ui, repo, message, match, opts):
401 401 """This is generic record driver.
402 402
403 403 It's job is to interactively filter local changes, and accordingly
404 404 prepare working dir into a state, where the job can be delegated to
405 405 non-interactive commit command such as 'commit' or 'qrefresh'.
406 406
407 407 After the actual job is done by non-interactive command, working dir
408 408 state is restored to original.
409 409
410 410 In the end we'll record intresting changes, and everything else will be
411 411 left in place, so the user can continue his work.
412 412 """
413 413
414 414 changes = repo.status(match=match)[:3]
415 415 diffopts = mdiff.diffopts(git=True, nodates=True)
416 416 chunks = patch.diff(repo, changes=changes, opts=diffopts)
417 417 fp = cStringIO.StringIO()
418 418 fp.write(''.join(chunks))
419 419 fp.seek(0)
420 420
421 421 # 1. filter patch, so we have intending-to apply subset of it
422 422 chunks = filterpatch(ui, parsepatch(fp))
423 423 del fp
424 424
425 425 contenders = {}
426 426 for h in chunks:
427 427 try: contenders.update(dict.fromkeys(h.files()))
428 428 except AttributeError: pass
429 429
430 430 changed = changes[0] + changes[1] + changes[2]
431 431 newfiles = [f for f in changed if f in contenders]
432 432 if not newfiles:
433 433 ui.status(_('no changes to record\n'))
434 434 return 0
435 435
436 436 modified = dict.fromkeys(changes[0])
437 437
438 438 # 2. backup changed files, so we can restore them in the end
439 439 backups = {}
440 440 backupdir = repo.join('record-backups')
441 441 try:
442 442 os.mkdir(backupdir)
443 443 except OSError, err:
444 444 if err.errno != errno.EEXIST:
445 445 raise
446 446 try:
447 447 # backup continues
448 448 for f in newfiles:
449 449 if f not in modified:
450 450 continue
451 451 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
452 452 dir=backupdir)
453 453 os.close(fd)
454 454 ui.debug(_('backup %r as %r\n') % (f, tmpname))
455 455 util.copyfile(repo.wjoin(f), tmpname)
456 456 backups[f] = tmpname
457 457
458 458 fp = cStringIO.StringIO()
459 459 for c in chunks:
460 460 if c.filename() in backups:
461 461 c.write(fp)
462 462 dopatch = fp.tell()
463 463 fp.seek(0)
464 464
465 465 # 3a. apply filtered patch to clean repo (clean)
466 466 if backups:
467 467 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
468 468
469 469 # 3b. (apply)
470 470 if dopatch:
471 471 try:
472 472 ui.debug(_('applying patch\n'))
473 473 ui.debug(fp.getvalue())
474 474 pfiles = {}
475 475 patch.internalpatch(fp, ui, 1, repo.root, files=pfiles)
476 476 patch.updatedir(ui, repo, pfiles)
477 477 except patch.PatchError, err:
478 478 s = str(err)
479 479 if s:
480 480 raise util.Abort(s)
481 481 else:
482 482 raise util.Abort(_('patch failed to apply'))
483 483 del fp
484 484
485 485 # 4. We prepared working directory according to filtered patch.
486 486 # Now is the time to delegate the job to commit/qrefresh or the like!
487 487
488 488 # it is important to first chdir to repo root -- we'll call a
489 489 # highlevel command with list of pathnames relative to repo root
490 490 cwd = os.getcwd()
491 491 os.chdir(repo.root)
492 492 try:
493 493 committer(ui, repo, newfiles, opts)
494 494 finally:
495 495 os.chdir(cwd)
496 496
497 497 return 0
498 498 finally:
499 499 # 5. finally restore backed-up files
500 500 try:
501 501 for realname, tmpname in backups.iteritems():
502 502 ui.debug(_('restoring %r to %r\n') % (tmpname, realname))
503 503 util.copyfile(tmpname, repo.wjoin(realname))
504 504 os.unlink(tmpname)
505 505 os.rmdir(backupdir)
506 506 except OSError:
507 507 pass
508 508 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
509 509
510 510 cmdtable = {
511 511 "record":
512 512 (record,
513 513
514 514 # add commit options
515 515 commands.table['^commit|ci'][1],
516 516
517 517 _('hg record [OPTION]... [FILE]...')),
518 518 }
519 519
520 520
521 521 def extsetup():
522 522 try:
523 523 mq = extensions.find('mq')
524 524 except KeyError:
525 525 return
526 526
527 527 qcmdtable = {
528 528 "qrecord":
529 529 (qrecord,
530 530
531 531 # add qnew options, except '--force'
532 532 [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'],
533 533
534 534 _('hg qrecord [OPTION]... PATCH [FILE]...')),
535 535 }
536 536
537 537 cmdtable.update(qcmdtable)
538 538
General Comments 0
You need to be logged in to leave comments. Login now