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