##// END OF EJS Templates
cmdutil: rename gettemplate() to _lookuplogtemplate()...
Yuya Nishihara -
r32833:edde2e97 default
parent child Browse files
Show More
@@ -1,3580 +1,3581 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import itertools
12 12 import os
13 13 import re
14 14 import tempfile
15 15
16 16 from .i18n import _
17 17 from .node import (
18 18 hex,
19 19 nullid,
20 20 nullrev,
21 21 short,
22 22 )
23 23
24 24 from . import (
25 25 bookmarks,
26 26 changelog,
27 27 copies,
28 28 crecord as crecordmod,
29 29 encoding,
30 30 error,
31 31 formatter,
32 32 graphmod,
33 33 lock as lockmod,
34 34 match as matchmod,
35 35 obsolete,
36 36 patch,
37 37 pathutil,
38 38 phases,
39 39 pycompat,
40 40 registrar,
41 41 repair,
42 42 revlog,
43 43 revset,
44 44 scmutil,
45 45 smartset,
46 46 templatekw,
47 47 templater,
48 48 util,
49 49 vfs as vfsmod,
50 50 )
51 51 stringio = util.stringio
52 52
53 53 # templates of common command options
54 54
55 55 dryrunopts = [
56 56 ('n', 'dry-run', None,
57 57 _('do not perform actions, just print output')),
58 58 ]
59 59
60 60 remoteopts = [
61 61 ('e', 'ssh', '',
62 62 _('specify ssh command to use'), _('CMD')),
63 63 ('', 'remotecmd', '',
64 64 _('specify hg command to run on the remote side'), _('CMD')),
65 65 ('', 'insecure', None,
66 66 _('do not verify server certificate (ignoring web.cacerts config)')),
67 67 ]
68 68
69 69 walkopts = [
70 70 ('I', 'include', [],
71 71 _('include names matching the given patterns'), _('PATTERN')),
72 72 ('X', 'exclude', [],
73 73 _('exclude names matching the given patterns'), _('PATTERN')),
74 74 ]
75 75
76 76 commitopts = [
77 77 ('m', 'message', '',
78 78 _('use text as commit message'), _('TEXT')),
79 79 ('l', 'logfile', '',
80 80 _('read commit message from file'), _('FILE')),
81 81 ]
82 82
83 83 commitopts2 = [
84 84 ('d', 'date', '',
85 85 _('record the specified date as commit date'), _('DATE')),
86 86 ('u', 'user', '',
87 87 _('record the specified user as committer'), _('USER')),
88 88 ]
89 89
90 90 # hidden for now
91 91 formatteropts = [
92 92 ('T', 'template', '',
93 93 _('display with template (EXPERIMENTAL)'), _('TEMPLATE')),
94 94 ]
95 95
96 96 templateopts = [
97 97 ('', 'style', '',
98 98 _('display using template map file (DEPRECATED)'), _('STYLE')),
99 99 ('T', 'template', '',
100 100 _('display with template'), _('TEMPLATE')),
101 101 ]
102 102
103 103 logopts = [
104 104 ('p', 'patch', None, _('show patch')),
105 105 ('g', 'git', None, _('use git extended diff format')),
106 106 ('l', 'limit', '',
107 107 _('limit number of changes displayed'), _('NUM')),
108 108 ('M', 'no-merges', None, _('do not show merges')),
109 109 ('', 'stat', None, _('output diffstat-style summary of changes')),
110 110 ('G', 'graph', None, _("show the revision DAG")),
111 111 ] + templateopts
112 112
113 113 diffopts = [
114 114 ('a', 'text', None, _('treat all files as text')),
115 115 ('g', 'git', None, _('use git extended diff format')),
116 116 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
117 117 ('', 'nodates', None, _('omit dates from diff headers'))
118 118 ]
119 119
120 120 diffwsopts = [
121 121 ('w', 'ignore-all-space', None,
122 122 _('ignore white space when comparing lines')),
123 123 ('b', 'ignore-space-change', None,
124 124 _('ignore changes in the amount of white space')),
125 125 ('B', 'ignore-blank-lines', None,
126 126 _('ignore changes whose lines are all blank')),
127 127 ]
128 128
129 129 diffopts2 = [
130 130 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
131 131 ('p', 'show-function', None, _('show which function each change is in')),
132 132 ('', 'reverse', None, _('produce a diff that undoes the changes')),
133 133 ] + diffwsopts + [
134 134 ('U', 'unified', '',
135 135 _('number of lines of context to show'), _('NUM')),
136 136 ('', 'stat', None, _('output diffstat-style summary of changes')),
137 137 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
138 138 ]
139 139
140 140 mergetoolopts = [
141 141 ('t', 'tool', '', _('specify merge tool')),
142 142 ]
143 143
144 144 similarityopts = [
145 145 ('s', 'similarity', '',
146 146 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
147 147 ]
148 148
149 149 subrepoopts = [
150 150 ('S', 'subrepos', None,
151 151 _('recurse into subrepositories'))
152 152 ]
153 153
154 154 debugrevlogopts = [
155 155 ('c', 'changelog', False, _('open changelog')),
156 156 ('m', 'manifest', False, _('open manifest')),
157 157 ('', 'dir', '', _('open directory manifest')),
158 158 ]
159 159
160 160 # special string such that everything below this line will be ingored in the
161 161 # editor text
162 162 _linebelow = "^HG: ------------------------ >8 ------------------------$"
163 163
164 164 def ishunk(x):
165 165 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
166 166 return isinstance(x, hunkclasses)
167 167
168 168 def newandmodified(chunks, originalchunks):
169 169 newlyaddedandmodifiedfiles = set()
170 170 for chunk in chunks:
171 171 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
172 172 originalchunks:
173 173 newlyaddedandmodifiedfiles.add(chunk.header.filename())
174 174 return newlyaddedandmodifiedfiles
175 175
176 176 def parsealiases(cmd):
177 177 return cmd.lstrip("^").split("|")
178 178
179 179 def setupwrapcolorwrite(ui):
180 180 # wrap ui.write so diff output can be labeled/colorized
181 181 def wrapwrite(orig, *args, **kw):
182 182 label = kw.pop('label', '')
183 183 for chunk, l in patch.difflabel(lambda: args):
184 184 orig(chunk, label=label + l)
185 185
186 186 oldwrite = ui.write
187 187 def wrap(*args, **kwargs):
188 188 return wrapwrite(oldwrite, *args, **kwargs)
189 189 setattr(ui, 'write', wrap)
190 190 return oldwrite
191 191
192 192 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
193 193 if usecurses:
194 194 if testfile:
195 195 recordfn = crecordmod.testdecorator(testfile,
196 196 crecordmod.testchunkselector)
197 197 else:
198 198 recordfn = crecordmod.chunkselector
199 199
200 200 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
201 201
202 202 else:
203 203 return patch.filterpatch(ui, originalhunks, operation)
204 204
205 205 def recordfilter(ui, originalhunks, operation=None):
206 206 """ Prompts the user to filter the originalhunks and return a list of
207 207 selected hunks.
208 208 *operation* is used for to build ui messages to indicate the user what
209 209 kind of filtering they are doing: reverting, committing, shelving, etc.
210 210 (see patch.filterpatch).
211 211 """
212 212 usecurses = crecordmod.checkcurses(ui)
213 213 testfile = ui.config('experimental', 'crecordtest', None)
214 214 oldwrite = setupwrapcolorwrite(ui)
215 215 try:
216 216 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
217 217 testfile, operation)
218 218 finally:
219 219 ui.write = oldwrite
220 220 return newchunks, newopts
221 221
222 222 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
223 223 filterfn, *pats, **opts):
224 224 from . import merge as mergemod
225 225 opts = pycompat.byteskwargs(opts)
226 226 if not ui.interactive():
227 227 if cmdsuggest:
228 228 msg = _('running non-interactively, use %s instead') % cmdsuggest
229 229 else:
230 230 msg = _('running non-interactively')
231 231 raise error.Abort(msg)
232 232
233 233 # make sure username is set before going interactive
234 234 if not opts.get('user'):
235 235 ui.username() # raise exception, username not provided
236 236
237 237 def recordfunc(ui, repo, message, match, opts):
238 238 """This is generic record driver.
239 239
240 240 Its job is to interactively filter local changes, and
241 241 accordingly prepare working directory into a state in which the
242 242 job can be delegated to a non-interactive commit command such as
243 243 'commit' or 'qrefresh'.
244 244
245 245 After the actual job is done by non-interactive command, the
246 246 working directory is restored to its original state.
247 247
248 248 In the end we'll record interesting changes, and everything else
249 249 will be left in place, so the user can continue working.
250 250 """
251 251
252 252 checkunfinished(repo, commit=True)
253 253 wctx = repo[None]
254 254 merge = len(wctx.parents()) > 1
255 255 if merge:
256 256 raise error.Abort(_('cannot partially commit a merge '
257 257 '(use "hg commit" instead)'))
258 258
259 259 def fail(f, msg):
260 260 raise error.Abort('%s: %s' % (f, msg))
261 261
262 262 force = opts.get('force')
263 263 if not force:
264 264 vdirs = []
265 265 match.explicitdir = vdirs.append
266 266 match.bad = fail
267 267
268 268 status = repo.status(match=match)
269 269 if not force:
270 270 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
271 271 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
272 272 diffopts.nodates = True
273 273 diffopts.git = True
274 274 diffopts.showfunc = True
275 275 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
276 276 originalchunks = patch.parsepatch(originaldiff)
277 277
278 278 # 1. filter patch, since we are intending to apply subset of it
279 279 try:
280 280 chunks, newopts = filterfn(ui, originalchunks)
281 281 except patch.PatchError as err:
282 282 raise error.Abort(_('error parsing patch: %s') % err)
283 283 opts.update(newopts)
284 284
285 285 # We need to keep a backup of files that have been newly added and
286 286 # modified during the recording process because there is a previous
287 287 # version without the edit in the workdir
288 288 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
289 289 contenders = set()
290 290 for h in chunks:
291 291 try:
292 292 contenders.update(set(h.files()))
293 293 except AttributeError:
294 294 pass
295 295
296 296 changed = status.modified + status.added + status.removed
297 297 newfiles = [f for f in changed if f in contenders]
298 298 if not newfiles:
299 299 ui.status(_('no changes to record\n'))
300 300 return 0
301 301
302 302 modified = set(status.modified)
303 303
304 304 # 2. backup changed files, so we can restore them in the end
305 305
306 306 if backupall:
307 307 tobackup = changed
308 308 else:
309 309 tobackup = [f for f in newfiles if f in modified or f in \
310 310 newlyaddedandmodifiedfiles]
311 311 backups = {}
312 312 if tobackup:
313 313 backupdir = repo.vfs.join('record-backups')
314 314 try:
315 315 os.mkdir(backupdir)
316 316 except OSError as err:
317 317 if err.errno != errno.EEXIST:
318 318 raise
319 319 try:
320 320 # backup continues
321 321 for f in tobackup:
322 322 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
323 323 dir=backupdir)
324 324 os.close(fd)
325 325 ui.debug('backup %r as %r\n' % (f, tmpname))
326 326 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
327 327 backups[f] = tmpname
328 328
329 329 fp = stringio()
330 330 for c in chunks:
331 331 fname = c.filename()
332 332 if fname in backups:
333 333 c.write(fp)
334 334 dopatch = fp.tell()
335 335 fp.seek(0)
336 336
337 337 # 2.5 optionally review / modify patch in text editor
338 338 if opts.get('review', False):
339 339 patchtext = (crecordmod.diffhelptext
340 340 + crecordmod.patchhelptext
341 341 + fp.read())
342 342 reviewedpatch = ui.edit(patchtext, "",
343 343 extra={"suffix": ".diff"},
344 344 repopath=repo.path)
345 345 fp.truncate(0)
346 346 fp.write(reviewedpatch)
347 347 fp.seek(0)
348 348
349 349 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
350 350 # 3a. apply filtered patch to clean repo (clean)
351 351 if backups:
352 352 # Equivalent to hg.revert
353 353 m = scmutil.matchfiles(repo, backups.keys())
354 354 mergemod.update(repo, repo.dirstate.p1(),
355 355 False, True, matcher=m)
356 356
357 357 # 3b. (apply)
358 358 if dopatch:
359 359 try:
360 360 ui.debug('applying patch\n')
361 361 ui.debug(fp.getvalue())
362 362 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
363 363 except patch.PatchError as err:
364 364 raise error.Abort(str(err))
365 365 del fp
366 366
367 367 # 4. We prepared working directory according to filtered
368 368 # patch. Now is the time to delegate the job to
369 369 # commit/qrefresh or the like!
370 370
371 371 # Make all of the pathnames absolute.
372 372 newfiles = [repo.wjoin(nf) for nf in newfiles]
373 373 return commitfunc(ui, repo, *newfiles, **opts)
374 374 finally:
375 375 # 5. finally restore backed-up files
376 376 try:
377 377 dirstate = repo.dirstate
378 378 for realname, tmpname in backups.iteritems():
379 379 ui.debug('restoring %r to %r\n' % (tmpname, realname))
380 380
381 381 if dirstate[realname] == 'n':
382 382 # without normallookup, restoring timestamp
383 383 # may cause partially committed files
384 384 # to be treated as unmodified
385 385 dirstate.normallookup(realname)
386 386
387 387 # copystat=True here and above are a hack to trick any
388 388 # editors that have f open that we haven't modified them.
389 389 #
390 390 # Also note that this racy as an editor could notice the
391 391 # file's mtime before we've finished writing it.
392 392 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
393 393 os.unlink(tmpname)
394 394 if tobackup:
395 395 os.rmdir(backupdir)
396 396 except OSError:
397 397 pass
398 398
399 399 def recordinwlock(ui, repo, message, match, opts):
400 400 with repo.wlock():
401 401 return recordfunc(ui, repo, message, match, opts)
402 402
403 403 return commit(ui, repo, recordinwlock, pats, opts)
404 404
405 405 def findpossible(cmd, table, strict=False):
406 406 """
407 407 Return cmd -> (aliases, command table entry)
408 408 for each matching command.
409 409 Return debug commands (or their aliases) only if no normal command matches.
410 410 """
411 411 choice = {}
412 412 debugchoice = {}
413 413
414 414 if cmd in table:
415 415 # short-circuit exact matches, "log" alias beats "^log|history"
416 416 keys = [cmd]
417 417 else:
418 418 keys = table.keys()
419 419
420 420 allcmds = []
421 421 for e in keys:
422 422 aliases = parsealiases(e)
423 423 allcmds.extend(aliases)
424 424 found = None
425 425 if cmd in aliases:
426 426 found = cmd
427 427 elif not strict:
428 428 for a in aliases:
429 429 if a.startswith(cmd):
430 430 found = a
431 431 break
432 432 if found is not None:
433 433 if aliases[0].startswith("debug") or found.startswith("debug"):
434 434 debugchoice[found] = (aliases, table[e])
435 435 else:
436 436 choice[found] = (aliases, table[e])
437 437
438 438 if not choice and debugchoice:
439 439 choice = debugchoice
440 440
441 441 return choice, allcmds
442 442
443 443 def findcmd(cmd, table, strict=True):
444 444 """Return (aliases, command table entry) for command string."""
445 445 choice, allcmds = findpossible(cmd, table, strict)
446 446
447 447 if cmd in choice:
448 448 return choice[cmd]
449 449
450 450 if len(choice) > 1:
451 451 clist = sorted(choice)
452 452 raise error.AmbiguousCommand(cmd, clist)
453 453
454 454 if choice:
455 455 return choice.values()[0]
456 456
457 457 raise error.UnknownCommand(cmd, allcmds)
458 458
459 459 def findrepo(p):
460 460 while not os.path.isdir(os.path.join(p, ".hg")):
461 461 oldp, p = p, os.path.dirname(p)
462 462 if p == oldp:
463 463 return None
464 464
465 465 return p
466 466
467 467 def bailifchanged(repo, merge=True, hint=None):
468 468 """ enforce the precondition that working directory must be clean.
469 469
470 470 'merge' can be set to false if a pending uncommitted merge should be
471 471 ignored (such as when 'update --check' runs).
472 472
473 473 'hint' is the usual hint given to Abort exception.
474 474 """
475 475
476 476 if merge and repo.dirstate.p2() != nullid:
477 477 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
478 478 modified, added, removed, deleted = repo.status()[:4]
479 479 if modified or added or removed or deleted:
480 480 raise error.Abort(_('uncommitted changes'), hint=hint)
481 481 ctx = repo[None]
482 482 for s in sorted(ctx.substate):
483 483 ctx.sub(s).bailifchanged(hint=hint)
484 484
485 485 def logmessage(ui, opts):
486 486 """ get the log message according to -m and -l option """
487 487 message = opts.get('message')
488 488 logfile = opts.get('logfile')
489 489
490 490 if message and logfile:
491 491 raise error.Abort(_('options --message and --logfile are mutually '
492 492 'exclusive'))
493 493 if not message and logfile:
494 494 try:
495 495 if isstdiofilename(logfile):
496 496 message = ui.fin.read()
497 497 else:
498 498 message = '\n'.join(util.readfile(logfile).splitlines())
499 499 except IOError as inst:
500 500 raise error.Abort(_("can't read commit message '%s': %s") %
501 501 (logfile, inst.strerror))
502 502 return message
503 503
504 504 def mergeeditform(ctxorbool, baseformname):
505 505 """return appropriate editform name (referencing a committemplate)
506 506
507 507 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
508 508 merging is committed.
509 509
510 510 This returns baseformname with '.merge' appended if it is a merge,
511 511 otherwise '.normal' is appended.
512 512 """
513 513 if isinstance(ctxorbool, bool):
514 514 if ctxorbool:
515 515 return baseformname + ".merge"
516 516 elif 1 < len(ctxorbool.parents()):
517 517 return baseformname + ".merge"
518 518
519 519 return baseformname + ".normal"
520 520
521 521 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
522 522 editform='', **opts):
523 523 """get appropriate commit message editor according to '--edit' option
524 524
525 525 'finishdesc' is a function to be called with edited commit message
526 526 (= 'description' of the new changeset) just after editing, but
527 527 before checking empty-ness. It should return actual text to be
528 528 stored into history. This allows to change description before
529 529 storing.
530 530
531 531 'extramsg' is a extra message to be shown in the editor instead of
532 532 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
533 533 is automatically added.
534 534
535 535 'editform' is a dot-separated list of names, to distinguish
536 536 the purpose of commit text editing.
537 537
538 538 'getcommiteditor' returns 'commitforceeditor' regardless of
539 539 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
540 540 they are specific for usage in MQ.
541 541 """
542 542 if edit or finishdesc or extramsg:
543 543 return lambda r, c, s: commitforceeditor(r, c, s,
544 544 finishdesc=finishdesc,
545 545 extramsg=extramsg,
546 546 editform=editform)
547 547 elif editform:
548 548 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
549 549 else:
550 550 return commiteditor
551 551
552 552 def loglimit(opts):
553 553 """get the log limit according to option -l/--limit"""
554 554 limit = opts.get('limit')
555 555 if limit:
556 556 try:
557 557 limit = int(limit)
558 558 except ValueError:
559 559 raise error.Abort(_('limit must be a positive integer'))
560 560 if limit <= 0:
561 561 raise error.Abort(_('limit must be positive'))
562 562 else:
563 563 limit = None
564 564 return limit
565 565
566 566 def makefilename(repo, pat, node, desc=None,
567 567 total=None, seqno=None, revwidth=None, pathname=None):
568 568 node_expander = {
569 569 'H': lambda: hex(node),
570 570 'R': lambda: str(repo.changelog.rev(node)),
571 571 'h': lambda: short(node),
572 572 'm': lambda: re.sub('[^\w]', '_', str(desc))
573 573 }
574 574 expander = {
575 575 '%': lambda: '%',
576 576 'b': lambda: os.path.basename(repo.root),
577 577 }
578 578
579 579 try:
580 580 if node:
581 581 expander.update(node_expander)
582 582 if node:
583 583 expander['r'] = (lambda:
584 584 str(repo.changelog.rev(node)).zfill(revwidth or 0))
585 585 if total is not None:
586 586 expander['N'] = lambda: str(total)
587 587 if seqno is not None:
588 588 expander['n'] = lambda: str(seqno)
589 589 if total is not None and seqno is not None:
590 590 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
591 591 if pathname is not None:
592 592 expander['s'] = lambda: os.path.basename(pathname)
593 593 expander['d'] = lambda: os.path.dirname(pathname) or '.'
594 594 expander['p'] = lambda: pathname
595 595
596 596 newname = []
597 597 patlen = len(pat)
598 598 i = 0
599 599 while i < patlen:
600 600 c = pat[i:i + 1]
601 601 if c == '%':
602 602 i += 1
603 603 c = pat[i:i + 1]
604 604 c = expander[c]()
605 605 newname.append(c)
606 606 i += 1
607 607 return ''.join(newname)
608 608 except KeyError as inst:
609 609 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
610 610 inst.args[0])
611 611
612 612 def isstdiofilename(pat):
613 613 """True if the given pat looks like a filename denoting stdin/stdout"""
614 614 return not pat or pat == '-'
615 615
616 616 class _unclosablefile(object):
617 617 def __init__(self, fp):
618 618 self._fp = fp
619 619
620 620 def close(self):
621 621 pass
622 622
623 623 def __iter__(self):
624 624 return iter(self._fp)
625 625
626 626 def __getattr__(self, attr):
627 627 return getattr(self._fp, attr)
628 628
629 629 def __enter__(self):
630 630 return self
631 631
632 632 def __exit__(self, exc_type, exc_value, exc_tb):
633 633 pass
634 634
635 635 def makefileobj(repo, pat, node=None, desc=None, total=None,
636 636 seqno=None, revwidth=None, mode='wb', modemap=None,
637 637 pathname=None):
638 638
639 639 writable = mode not in ('r', 'rb')
640 640
641 641 if isstdiofilename(pat):
642 642 if writable:
643 643 fp = repo.ui.fout
644 644 else:
645 645 fp = repo.ui.fin
646 646 return _unclosablefile(fp)
647 647 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
648 648 if modemap is not None:
649 649 mode = modemap.get(fn, mode)
650 650 if mode == 'wb':
651 651 modemap[fn] = 'ab'
652 652 return open(fn, mode)
653 653
654 654 def openrevlog(repo, cmd, file_, opts):
655 655 """opens the changelog, manifest, a filelog or a given revlog"""
656 656 cl = opts['changelog']
657 657 mf = opts['manifest']
658 658 dir = opts['dir']
659 659 msg = None
660 660 if cl and mf:
661 661 msg = _('cannot specify --changelog and --manifest at the same time')
662 662 elif cl and dir:
663 663 msg = _('cannot specify --changelog and --dir at the same time')
664 664 elif cl or mf or dir:
665 665 if file_:
666 666 msg = _('cannot specify filename with --changelog or --manifest')
667 667 elif not repo:
668 668 msg = _('cannot specify --changelog or --manifest or --dir '
669 669 'without a repository')
670 670 if msg:
671 671 raise error.Abort(msg)
672 672
673 673 r = None
674 674 if repo:
675 675 if cl:
676 676 r = repo.unfiltered().changelog
677 677 elif dir:
678 678 if 'treemanifest' not in repo.requirements:
679 679 raise error.Abort(_("--dir can only be used on repos with "
680 680 "treemanifest enabled"))
681 681 dirlog = repo.manifestlog._revlog.dirlog(dir)
682 682 if len(dirlog):
683 683 r = dirlog
684 684 elif mf:
685 685 r = repo.manifestlog._revlog
686 686 elif file_:
687 687 filelog = repo.file(file_)
688 688 if len(filelog):
689 689 r = filelog
690 690 if not r:
691 691 if not file_:
692 692 raise error.CommandError(cmd, _('invalid arguments'))
693 693 if not os.path.isfile(file_):
694 694 raise error.Abort(_("revlog '%s' not found") % file_)
695 695 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
696 696 file_[:-2] + ".i")
697 697 return r
698 698
699 699 def copy(ui, repo, pats, opts, rename=False):
700 700 # called with the repo lock held
701 701 #
702 702 # hgsep => pathname that uses "/" to separate directories
703 703 # ossep => pathname that uses os.sep to separate directories
704 704 cwd = repo.getcwd()
705 705 targets = {}
706 706 after = opts.get("after")
707 707 dryrun = opts.get("dry_run")
708 708 wctx = repo[None]
709 709
710 710 def walkpat(pat):
711 711 srcs = []
712 712 if after:
713 713 badstates = '?'
714 714 else:
715 715 badstates = '?r'
716 716 m = scmutil.match(wctx, [pat], opts, globbed=True)
717 717 for abs in wctx.walk(m):
718 718 state = repo.dirstate[abs]
719 719 rel = m.rel(abs)
720 720 exact = m.exact(abs)
721 721 if state in badstates:
722 722 if exact and state == '?':
723 723 ui.warn(_('%s: not copying - file is not managed\n') % rel)
724 724 if exact and state == 'r':
725 725 ui.warn(_('%s: not copying - file has been marked for'
726 726 ' remove\n') % rel)
727 727 continue
728 728 # abs: hgsep
729 729 # rel: ossep
730 730 srcs.append((abs, rel, exact))
731 731 return srcs
732 732
733 733 # abssrc: hgsep
734 734 # relsrc: ossep
735 735 # otarget: ossep
736 736 def copyfile(abssrc, relsrc, otarget, exact):
737 737 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
738 738 if '/' in abstarget:
739 739 # We cannot normalize abstarget itself, this would prevent
740 740 # case only renames, like a => A.
741 741 abspath, absname = abstarget.rsplit('/', 1)
742 742 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
743 743 reltarget = repo.pathto(abstarget, cwd)
744 744 target = repo.wjoin(abstarget)
745 745 src = repo.wjoin(abssrc)
746 746 state = repo.dirstate[abstarget]
747 747
748 748 scmutil.checkportable(ui, abstarget)
749 749
750 750 # check for collisions
751 751 prevsrc = targets.get(abstarget)
752 752 if prevsrc is not None:
753 753 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
754 754 (reltarget, repo.pathto(abssrc, cwd),
755 755 repo.pathto(prevsrc, cwd)))
756 756 return
757 757
758 758 # check for overwrites
759 759 exists = os.path.lexists(target)
760 760 samefile = False
761 761 if exists and abssrc != abstarget:
762 762 if (repo.dirstate.normalize(abssrc) ==
763 763 repo.dirstate.normalize(abstarget)):
764 764 if not rename:
765 765 ui.warn(_("%s: can't copy - same file\n") % reltarget)
766 766 return
767 767 exists = False
768 768 samefile = True
769 769
770 770 if not after and exists or after and state in 'mn':
771 771 if not opts['force']:
772 772 if state in 'mn':
773 773 msg = _('%s: not overwriting - file already committed\n')
774 774 if after:
775 775 flags = '--after --force'
776 776 else:
777 777 flags = '--force'
778 778 if rename:
779 779 hint = _('(hg rename %s to replace the file by '
780 780 'recording a rename)\n') % flags
781 781 else:
782 782 hint = _('(hg copy %s to replace the file by '
783 783 'recording a copy)\n') % flags
784 784 else:
785 785 msg = _('%s: not overwriting - file exists\n')
786 786 if rename:
787 787 hint = _('(hg rename --after to record the rename)\n')
788 788 else:
789 789 hint = _('(hg copy --after to record the copy)\n')
790 790 ui.warn(msg % reltarget)
791 791 ui.warn(hint)
792 792 return
793 793
794 794 if after:
795 795 if not exists:
796 796 if rename:
797 797 ui.warn(_('%s: not recording move - %s does not exist\n') %
798 798 (relsrc, reltarget))
799 799 else:
800 800 ui.warn(_('%s: not recording copy - %s does not exist\n') %
801 801 (relsrc, reltarget))
802 802 return
803 803 elif not dryrun:
804 804 try:
805 805 if exists:
806 806 os.unlink(target)
807 807 targetdir = os.path.dirname(target) or '.'
808 808 if not os.path.isdir(targetdir):
809 809 os.makedirs(targetdir)
810 810 if samefile:
811 811 tmp = target + "~hgrename"
812 812 os.rename(src, tmp)
813 813 os.rename(tmp, target)
814 814 else:
815 815 util.copyfile(src, target)
816 816 srcexists = True
817 817 except IOError as inst:
818 818 if inst.errno == errno.ENOENT:
819 819 ui.warn(_('%s: deleted in working directory\n') % relsrc)
820 820 srcexists = False
821 821 else:
822 822 ui.warn(_('%s: cannot copy - %s\n') %
823 823 (relsrc, inst.strerror))
824 824 return True # report a failure
825 825
826 826 if ui.verbose or not exact:
827 827 if rename:
828 828 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
829 829 else:
830 830 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
831 831
832 832 targets[abstarget] = abssrc
833 833
834 834 # fix up dirstate
835 835 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
836 836 dryrun=dryrun, cwd=cwd)
837 837 if rename and not dryrun:
838 838 if not after and srcexists and not samefile:
839 839 repo.wvfs.unlinkpath(abssrc)
840 840 wctx.forget([abssrc])
841 841
842 842 # pat: ossep
843 843 # dest ossep
844 844 # srcs: list of (hgsep, hgsep, ossep, bool)
845 845 # return: function that takes hgsep and returns ossep
846 846 def targetpathfn(pat, dest, srcs):
847 847 if os.path.isdir(pat):
848 848 abspfx = pathutil.canonpath(repo.root, cwd, pat)
849 849 abspfx = util.localpath(abspfx)
850 850 if destdirexists:
851 851 striplen = len(os.path.split(abspfx)[0])
852 852 else:
853 853 striplen = len(abspfx)
854 854 if striplen:
855 855 striplen += len(pycompat.ossep)
856 856 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
857 857 elif destdirexists:
858 858 res = lambda p: os.path.join(dest,
859 859 os.path.basename(util.localpath(p)))
860 860 else:
861 861 res = lambda p: dest
862 862 return res
863 863
864 864 # pat: ossep
865 865 # dest ossep
866 866 # srcs: list of (hgsep, hgsep, ossep, bool)
867 867 # return: function that takes hgsep and returns ossep
868 868 def targetpathafterfn(pat, dest, srcs):
869 869 if matchmod.patkind(pat):
870 870 # a mercurial pattern
871 871 res = lambda p: os.path.join(dest,
872 872 os.path.basename(util.localpath(p)))
873 873 else:
874 874 abspfx = pathutil.canonpath(repo.root, cwd, pat)
875 875 if len(abspfx) < len(srcs[0][0]):
876 876 # A directory. Either the target path contains the last
877 877 # component of the source path or it does not.
878 878 def evalpath(striplen):
879 879 score = 0
880 880 for s in srcs:
881 881 t = os.path.join(dest, util.localpath(s[0])[striplen:])
882 882 if os.path.lexists(t):
883 883 score += 1
884 884 return score
885 885
886 886 abspfx = util.localpath(abspfx)
887 887 striplen = len(abspfx)
888 888 if striplen:
889 889 striplen += len(pycompat.ossep)
890 890 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
891 891 score = evalpath(striplen)
892 892 striplen1 = len(os.path.split(abspfx)[0])
893 893 if striplen1:
894 894 striplen1 += len(pycompat.ossep)
895 895 if evalpath(striplen1) > score:
896 896 striplen = striplen1
897 897 res = lambda p: os.path.join(dest,
898 898 util.localpath(p)[striplen:])
899 899 else:
900 900 # a file
901 901 if destdirexists:
902 902 res = lambda p: os.path.join(dest,
903 903 os.path.basename(util.localpath(p)))
904 904 else:
905 905 res = lambda p: dest
906 906 return res
907 907
908 908 pats = scmutil.expandpats(pats)
909 909 if not pats:
910 910 raise error.Abort(_('no source or destination specified'))
911 911 if len(pats) == 1:
912 912 raise error.Abort(_('no destination specified'))
913 913 dest = pats.pop()
914 914 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
915 915 if not destdirexists:
916 916 if len(pats) > 1 or matchmod.patkind(pats[0]):
917 917 raise error.Abort(_('with multiple sources, destination must be an '
918 918 'existing directory'))
919 919 if util.endswithsep(dest):
920 920 raise error.Abort(_('destination %s is not a directory') % dest)
921 921
922 922 tfn = targetpathfn
923 923 if after:
924 924 tfn = targetpathafterfn
925 925 copylist = []
926 926 for pat in pats:
927 927 srcs = walkpat(pat)
928 928 if not srcs:
929 929 continue
930 930 copylist.append((tfn(pat, dest, srcs), srcs))
931 931 if not copylist:
932 932 raise error.Abort(_('no files to copy'))
933 933
934 934 errors = 0
935 935 for targetpath, srcs in copylist:
936 936 for abssrc, relsrc, exact in srcs:
937 937 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
938 938 errors += 1
939 939
940 940 if errors:
941 941 ui.warn(_('(consider using --after)\n'))
942 942
943 943 return errors != 0
944 944
945 945 ## facility to let extension process additional data into an import patch
946 946 # list of identifier to be executed in order
947 947 extrapreimport = [] # run before commit
948 948 extrapostimport = [] # run after commit
949 949 # mapping from identifier to actual import function
950 950 #
951 951 # 'preimport' are run before the commit is made and are provided the following
952 952 # arguments:
953 953 # - repo: the localrepository instance,
954 954 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
955 955 # - extra: the future extra dictionary of the changeset, please mutate it,
956 956 # - opts: the import options.
957 957 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
958 958 # mutation of in memory commit and more. Feel free to rework the code to get
959 959 # there.
960 960 extrapreimportmap = {}
961 961 # 'postimport' are run after the commit is made and are provided the following
962 962 # argument:
963 963 # - ctx: the changectx created by import.
964 964 extrapostimportmap = {}
965 965
966 966 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
967 967 """Utility function used by commands.import to import a single patch
968 968
969 969 This function is explicitly defined here to help the evolve extension to
970 970 wrap this part of the import logic.
971 971
972 972 The API is currently a bit ugly because it a simple code translation from
973 973 the import command. Feel free to make it better.
974 974
975 975 :hunk: a patch (as a binary string)
976 976 :parents: nodes that will be parent of the created commit
977 977 :opts: the full dict of option passed to the import command
978 978 :msgs: list to save commit message to.
979 979 (used in case we need to save it when failing)
980 980 :updatefunc: a function that update a repo to a given node
981 981 updatefunc(<repo>, <node>)
982 982 """
983 983 # avoid cycle context -> subrepo -> cmdutil
984 984 from . import context
985 985 extractdata = patch.extract(ui, hunk)
986 986 tmpname = extractdata.get('filename')
987 987 message = extractdata.get('message')
988 988 user = opts.get('user') or extractdata.get('user')
989 989 date = opts.get('date') or extractdata.get('date')
990 990 branch = extractdata.get('branch')
991 991 nodeid = extractdata.get('nodeid')
992 992 p1 = extractdata.get('p1')
993 993 p2 = extractdata.get('p2')
994 994
995 995 nocommit = opts.get('no_commit')
996 996 importbranch = opts.get('import_branch')
997 997 update = not opts.get('bypass')
998 998 strip = opts["strip"]
999 999 prefix = opts["prefix"]
1000 1000 sim = float(opts.get('similarity') or 0)
1001 1001 if not tmpname:
1002 1002 return (None, None, False)
1003 1003
1004 1004 rejects = False
1005 1005
1006 1006 try:
1007 1007 cmdline_message = logmessage(ui, opts)
1008 1008 if cmdline_message:
1009 1009 # pickup the cmdline msg
1010 1010 message = cmdline_message
1011 1011 elif message:
1012 1012 # pickup the patch msg
1013 1013 message = message.strip()
1014 1014 else:
1015 1015 # launch the editor
1016 1016 message = None
1017 1017 ui.debug('message:\n%s\n' % message)
1018 1018
1019 1019 if len(parents) == 1:
1020 1020 parents.append(repo[nullid])
1021 1021 if opts.get('exact'):
1022 1022 if not nodeid or not p1:
1023 1023 raise error.Abort(_('not a Mercurial patch'))
1024 1024 p1 = repo[p1]
1025 1025 p2 = repo[p2 or nullid]
1026 1026 elif p2:
1027 1027 try:
1028 1028 p1 = repo[p1]
1029 1029 p2 = repo[p2]
1030 1030 # Without any options, consider p2 only if the
1031 1031 # patch is being applied on top of the recorded
1032 1032 # first parent.
1033 1033 if p1 != parents[0]:
1034 1034 p1 = parents[0]
1035 1035 p2 = repo[nullid]
1036 1036 except error.RepoError:
1037 1037 p1, p2 = parents
1038 1038 if p2.node() == nullid:
1039 1039 ui.warn(_("warning: import the patch as a normal revision\n"
1040 1040 "(use --exact to import the patch as a merge)\n"))
1041 1041 else:
1042 1042 p1, p2 = parents
1043 1043
1044 1044 n = None
1045 1045 if update:
1046 1046 if p1 != parents[0]:
1047 1047 updatefunc(repo, p1.node())
1048 1048 if p2 != parents[1]:
1049 1049 repo.setparents(p1.node(), p2.node())
1050 1050
1051 1051 if opts.get('exact') or importbranch:
1052 1052 repo.dirstate.setbranch(branch or 'default')
1053 1053
1054 1054 partial = opts.get('partial', False)
1055 1055 files = set()
1056 1056 try:
1057 1057 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1058 1058 files=files, eolmode=None, similarity=sim / 100.0)
1059 1059 except patch.PatchError as e:
1060 1060 if not partial:
1061 1061 raise error.Abort(str(e))
1062 1062 if partial:
1063 1063 rejects = True
1064 1064
1065 1065 files = list(files)
1066 1066 if nocommit:
1067 1067 if message:
1068 1068 msgs.append(message)
1069 1069 else:
1070 1070 if opts.get('exact') or p2:
1071 1071 # If you got here, you either use --force and know what
1072 1072 # you are doing or used --exact or a merge patch while
1073 1073 # being updated to its first parent.
1074 1074 m = None
1075 1075 else:
1076 1076 m = scmutil.matchfiles(repo, files or [])
1077 1077 editform = mergeeditform(repo[None], 'import.normal')
1078 1078 if opts.get('exact'):
1079 1079 editor = None
1080 1080 else:
1081 1081 editor = getcommiteditor(editform=editform, **opts)
1082 1082 extra = {}
1083 1083 for idfunc in extrapreimport:
1084 1084 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
1085 1085 overrides = {}
1086 1086 if partial:
1087 1087 overrides[('ui', 'allowemptycommit')] = True
1088 1088 with repo.ui.configoverride(overrides, 'import'):
1089 1089 n = repo.commit(message, user,
1090 1090 date, match=m,
1091 1091 editor=editor, extra=extra)
1092 1092 for idfunc in extrapostimport:
1093 1093 extrapostimportmap[idfunc](repo[n])
1094 1094 else:
1095 1095 if opts.get('exact') or importbranch:
1096 1096 branch = branch or 'default'
1097 1097 else:
1098 1098 branch = p1.branch()
1099 1099 store = patch.filestore()
1100 1100 try:
1101 1101 files = set()
1102 1102 try:
1103 1103 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1104 1104 files, eolmode=None)
1105 1105 except patch.PatchError as e:
1106 1106 raise error.Abort(str(e))
1107 1107 if opts.get('exact'):
1108 1108 editor = None
1109 1109 else:
1110 1110 editor = getcommiteditor(editform='import.bypass')
1111 1111 memctx = context.memctx(repo, (p1.node(), p2.node()),
1112 1112 message,
1113 1113 files=files,
1114 1114 filectxfn=store,
1115 1115 user=user,
1116 1116 date=date,
1117 1117 branch=branch,
1118 1118 editor=editor)
1119 1119 n = memctx.commit()
1120 1120 finally:
1121 1121 store.close()
1122 1122 if opts.get('exact') and nocommit:
1123 1123 # --exact with --no-commit is still useful in that it does merge
1124 1124 # and branch bits
1125 1125 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1126 1126 elif opts.get('exact') and hex(n) != nodeid:
1127 1127 raise error.Abort(_('patch is damaged or loses information'))
1128 1128 msg = _('applied to working directory')
1129 1129 if n:
1130 1130 # i18n: refers to a short changeset id
1131 1131 msg = _('created %s') % short(n)
1132 1132 return (msg, n, rejects)
1133 1133 finally:
1134 1134 os.unlink(tmpname)
1135 1135
1136 1136 # facility to let extensions include additional data in an exported patch
1137 1137 # list of identifiers to be executed in order
1138 1138 extraexport = []
1139 1139 # mapping from identifier to actual export function
1140 1140 # function as to return a string to be added to the header or None
1141 1141 # it is given two arguments (sequencenumber, changectx)
1142 1142 extraexportmap = {}
1143 1143
1144 1144 def _exportsingle(repo, ctx, match, switch_parent, rev, seqno, write, diffopts):
1145 1145 node = scmutil.binnode(ctx)
1146 1146 parents = [p.node() for p in ctx.parents() if p]
1147 1147 branch = ctx.branch()
1148 1148 if switch_parent:
1149 1149 parents.reverse()
1150 1150
1151 1151 if parents:
1152 1152 prev = parents[0]
1153 1153 else:
1154 1154 prev = nullid
1155 1155
1156 1156 write("# HG changeset patch\n")
1157 1157 write("# User %s\n" % ctx.user())
1158 1158 write("# Date %d %d\n" % ctx.date())
1159 1159 write("# %s\n" % util.datestr(ctx.date()))
1160 1160 if branch and branch != 'default':
1161 1161 write("# Branch %s\n" % branch)
1162 1162 write("# Node ID %s\n" % hex(node))
1163 1163 write("# Parent %s\n" % hex(prev))
1164 1164 if len(parents) > 1:
1165 1165 write("# Parent %s\n" % hex(parents[1]))
1166 1166
1167 1167 for headerid in extraexport:
1168 1168 header = extraexportmap[headerid](seqno, ctx)
1169 1169 if header is not None:
1170 1170 write('# %s\n' % header)
1171 1171 write(ctx.description().rstrip())
1172 1172 write("\n\n")
1173 1173
1174 1174 for chunk, label in patch.diffui(repo, prev, node, match, opts=diffopts):
1175 1175 write(chunk, label=label)
1176 1176
1177 1177 def export(repo, revs, fntemplate='hg-%h.patch', fp=None, switch_parent=False,
1178 1178 opts=None, match=None):
1179 1179 '''export changesets as hg patches
1180 1180
1181 1181 Args:
1182 1182 repo: The repository from which we're exporting revisions.
1183 1183 revs: A list of revisions to export as revision numbers.
1184 1184 fntemplate: An optional string to use for generating patch file names.
1185 1185 fp: An optional file-like object to which patches should be written.
1186 1186 switch_parent: If True, show diffs against second parent when not nullid.
1187 1187 Default is false, which always shows diff against p1.
1188 1188 opts: diff options to use for generating the patch.
1189 1189 match: If specified, only export changes to files matching this matcher.
1190 1190
1191 1191 Returns:
1192 1192 Nothing.
1193 1193
1194 1194 Side Effect:
1195 1195 "HG Changeset Patch" data is emitted to one of the following
1196 1196 destinations:
1197 1197 fp is specified: All revs are written to the specified
1198 1198 file-like object.
1199 1199 fntemplate specified: Each rev is written to a unique file named using
1200 1200 the given template.
1201 1201 Neither fp nor template specified: All revs written to repo.ui.write()
1202 1202 '''
1203 1203
1204 1204 total = len(revs)
1205 1205 revwidth = max(len(str(rev)) for rev in revs)
1206 1206 filemode = {}
1207 1207
1208 1208 write = None
1209 1209 dest = '<unnamed>'
1210 1210 if fp:
1211 1211 dest = getattr(fp, 'name', dest)
1212 1212 def write(s, **kw):
1213 1213 fp.write(s)
1214 1214 elif not fntemplate:
1215 1215 write = repo.ui.write
1216 1216
1217 1217 for seqno, rev in enumerate(revs, 1):
1218 1218 ctx = repo[rev]
1219 1219 fo = None
1220 1220 if not fp and fntemplate:
1221 1221 desc_lines = ctx.description().rstrip().split('\n')
1222 1222 desc = desc_lines[0] #Commit always has a first line.
1223 1223 fo = makefileobj(repo, fntemplate, ctx.node(), desc=desc,
1224 1224 total=total, seqno=seqno, revwidth=revwidth,
1225 1225 mode='wb', modemap=filemode)
1226 1226 dest = fo.name
1227 1227 def write(s, **kw):
1228 1228 fo.write(s)
1229 1229 if not dest.startswith('<'):
1230 1230 repo.ui.note("%s\n" % dest)
1231 1231 _exportsingle(
1232 1232 repo, ctx, match, switch_parent, rev, seqno, write, opts)
1233 1233 if fo is not None:
1234 1234 fo.close()
1235 1235
1236 1236 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1237 1237 changes=None, stat=False, fp=None, prefix='',
1238 1238 root='', listsubrepos=False):
1239 1239 '''show diff or diffstat.'''
1240 1240 if fp is None:
1241 1241 write = ui.write
1242 1242 else:
1243 1243 def write(s, **kw):
1244 1244 fp.write(s)
1245 1245
1246 1246 if root:
1247 1247 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1248 1248 else:
1249 1249 relroot = ''
1250 1250 if relroot != '':
1251 1251 # XXX relative roots currently don't work if the root is within a
1252 1252 # subrepo
1253 1253 uirelroot = match.uipath(relroot)
1254 1254 relroot += '/'
1255 1255 for matchroot in match.files():
1256 1256 if not matchroot.startswith(relroot):
1257 1257 ui.warn(_('warning: %s not inside relative root %s\n') % (
1258 1258 match.uipath(matchroot), uirelroot))
1259 1259
1260 1260 if stat:
1261 1261 diffopts = diffopts.copy(context=0)
1262 1262 width = 80
1263 1263 if not ui.plain():
1264 1264 width = ui.termwidth()
1265 1265 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1266 1266 prefix=prefix, relroot=relroot)
1267 1267 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1268 1268 width=width):
1269 1269 write(chunk, label=label)
1270 1270 else:
1271 1271 for chunk, label in patch.diffui(repo, node1, node2, match,
1272 1272 changes, diffopts, prefix=prefix,
1273 1273 relroot=relroot):
1274 1274 write(chunk, label=label)
1275 1275
1276 1276 if listsubrepos:
1277 1277 ctx1 = repo[node1]
1278 1278 ctx2 = repo[node2]
1279 1279 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1280 1280 tempnode2 = node2
1281 1281 try:
1282 1282 if node2 is not None:
1283 1283 tempnode2 = ctx2.substate[subpath][1]
1284 1284 except KeyError:
1285 1285 # A subrepo that existed in node1 was deleted between node1 and
1286 1286 # node2 (inclusive). Thus, ctx2's substate won't contain that
1287 1287 # subpath. The best we can do is to ignore it.
1288 1288 tempnode2 = None
1289 1289 submatch = matchmod.subdirmatcher(subpath, match)
1290 1290 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1291 1291 stat=stat, fp=fp, prefix=prefix)
1292 1292
1293 1293 def _changesetlabels(ctx):
1294 1294 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1295 1295 if ctx.obsolete():
1296 1296 labels.append('changeset.obsolete')
1297 1297 if ctx.troubled():
1298 1298 labels.append('changeset.troubled')
1299 1299 for trouble in ctx.troubles():
1300 1300 labels.append('trouble.%s' % trouble)
1301 1301 return ' '.join(labels)
1302 1302
1303 1303 class changeset_printer(object):
1304 1304 '''show changeset information when templating not requested.'''
1305 1305
1306 1306 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1307 1307 self.ui = ui
1308 1308 self.repo = repo
1309 1309 self.buffered = buffered
1310 1310 self.matchfn = matchfn
1311 1311 self.diffopts = diffopts
1312 1312 self.header = {}
1313 1313 self.hunk = {}
1314 1314 self.lastheader = None
1315 1315 self.footer = None
1316 1316
1317 1317 def flush(self, ctx):
1318 1318 rev = ctx.rev()
1319 1319 if rev in self.header:
1320 1320 h = self.header[rev]
1321 1321 if h != self.lastheader:
1322 1322 self.lastheader = h
1323 1323 self.ui.write(h)
1324 1324 del self.header[rev]
1325 1325 if rev in self.hunk:
1326 1326 self.ui.write(self.hunk[rev])
1327 1327 del self.hunk[rev]
1328 1328 return 1
1329 1329 return 0
1330 1330
1331 1331 def close(self):
1332 1332 if self.footer:
1333 1333 self.ui.write(self.footer)
1334 1334
1335 1335 def show(self, ctx, copies=None, matchfn=None, **props):
1336 1336 if self.buffered:
1337 1337 self.ui.pushbuffer(labeled=True)
1338 1338 self._show(ctx, copies, matchfn, props)
1339 1339 self.hunk[ctx.rev()] = self.ui.popbuffer()
1340 1340 else:
1341 1341 self._show(ctx, copies, matchfn, props)
1342 1342
1343 1343 def _show(self, ctx, copies, matchfn, props):
1344 1344 '''show a single changeset or file revision'''
1345 1345 changenode = ctx.node()
1346 1346 rev = ctx.rev()
1347 1347 if self.ui.debugflag:
1348 1348 hexfunc = hex
1349 1349 else:
1350 1350 hexfunc = short
1351 1351 # as of now, wctx.node() and wctx.rev() return None, but we want to
1352 1352 # show the same values as {node} and {rev} templatekw
1353 1353 revnode = (scmutil.intrev(ctx), hexfunc(scmutil.binnode(ctx)))
1354 1354
1355 1355 if self.ui.quiet:
1356 1356 self.ui.write("%d:%s\n" % revnode, label='log.node')
1357 1357 return
1358 1358
1359 1359 date = util.datestr(ctx.date())
1360 1360
1361 1361 # i18n: column positioning for "hg log"
1362 1362 self.ui.write(_("changeset: %d:%s\n") % revnode,
1363 1363 label=_changesetlabels(ctx))
1364 1364
1365 1365 # branches are shown first before any other names due to backwards
1366 1366 # compatibility
1367 1367 branch = ctx.branch()
1368 1368 # don't show the default branch name
1369 1369 if branch != 'default':
1370 1370 # i18n: column positioning for "hg log"
1371 1371 self.ui.write(_("branch: %s\n") % branch,
1372 1372 label='log.branch')
1373 1373
1374 1374 for nsname, ns in self.repo.names.iteritems():
1375 1375 # branches has special logic already handled above, so here we just
1376 1376 # skip it
1377 1377 if nsname == 'branches':
1378 1378 continue
1379 1379 # we will use the templatename as the color name since those two
1380 1380 # should be the same
1381 1381 for name in ns.names(self.repo, changenode):
1382 1382 self.ui.write(ns.logfmt % name,
1383 1383 label='log.%s' % ns.colorname)
1384 1384 if self.ui.debugflag:
1385 1385 # i18n: column positioning for "hg log"
1386 1386 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1387 1387 label='log.phase')
1388 1388 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1389 1389 label = 'log.parent changeset.%s' % pctx.phasestr()
1390 1390 # i18n: column positioning for "hg log"
1391 1391 self.ui.write(_("parent: %d:%s\n")
1392 1392 % (pctx.rev(), hexfunc(pctx.node())),
1393 1393 label=label)
1394 1394
1395 1395 if self.ui.debugflag and rev is not None:
1396 1396 mnode = ctx.manifestnode()
1397 1397 # i18n: column positioning for "hg log"
1398 1398 self.ui.write(_("manifest: %d:%s\n") %
1399 1399 (self.repo.manifestlog._revlog.rev(mnode),
1400 1400 hex(mnode)),
1401 1401 label='ui.debug log.manifest')
1402 1402 # i18n: column positioning for "hg log"
1403 1403 self.ui.write(_("user: %s\n") % ctx.user(),
1404 1404 label='log.user')
1405 1405 # i18n: column positioning for "hg log"
1406 1406 self.ui.write(_("date: %s\n") % date,
1407 1407 label='log.date')
1408 1408
1409 1409 if ctx.troubled():
1410 1410 # i18n: column positioning for "hg log"
1411 1411 self.ui.write(_("trouble: %s\n") % ', '.join(ctx.troubles()),
1412 1412 label='log.trouble')
1413 1413
1414 1414 if self.ui.debugflag:
1415 1415 files = ctx.p1().status(ctx)[:3]
1416 1416 for key, value in zip([# i18n: column positioning for "hg log"
1417 1417 _("files:"),
1418 1418 # i18n: column positioning for "hg log"
1419 1419 _("files+:"),
1420 1420 # i18n: column positioning for "hg log"
1421 1421 _("files-:")], files):
1422 1422 if value:
1423 1423 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1424 1424 label='ui.debug log.files')
1425 1425 elif ctx.files() and self.ui.verbose:
1426 1426 # i18n: column positioning for "hg log"
1427 1427 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1428 1428 label='ui.note log.files')
1429 1429 if copies and self.ui.verbose:
1430 1430 copies = ['%s (%s)' % c for c in copies]
1431 1431 # i18n: column positioning for "hg log"
1432 1432 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1433 1433 label='ui.note log.copies')
1434 1434
1435 1435 extra = ctx.extra()
1436 1436 if extra and self.ui.debugflag:
1437 1437 for key, value in sorted(extra.items()):
1438 1438 # i18n: column positioning for "hg log"
1439 1439 self.ui.write(_("extra: %s=%s\n")
1440 1440 % (key, util.escapestr(value)),
1441 1441 label='ui.debug log.extra')
1442 1442
1443 1443 description = ctx.description().strip()
1444 1444 if description:
1445 1445 if self.ui.verbose:
1446 1446 self.ui.write(_("description:\n"),
1447 1447 label='ui.note log.description')
1448 1448 self.ui.write(description,
1449 1449 label='ui.note log.description')
1450 1450 self.ui.write("\n\n")
1451 1451 else:
1452 1452 # i18n: column positioning for "hg log"
1453 1453 self.ui.write(_("summary: %s\n") %
1454 1454 description.splitlines()[0],
1455 1455 label='log.summary')
1456 1456 self.ui.write("\n")
1457 1457
1458 1458 self.showpatch(ctx, matchfn)
1459 1459
1460 1460 def showpatch(self, ctx, matchfn):
1461 1461 if not matchfn:
1462 1462 matchfn = self.matchfn
1463 1463 if matchfn:
1464 1464 stat = self.diffopts.get('stat')
1465 1465 diff = self.diffopts.get('patch')
1466 1466 diffopts = patch.diffallopts(self.ui, self.diffopts)
1467 1467 node = ctx.node()
1468 1468 prev = ctx.p1().node()
1469 1469 if stat:
1470 1470 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1471 1471 match=matchfn, stat=True)
1472 1472 if diff:
1473 1473 if stat:
1474 1474 self.ui.write("\n")
1475 1475 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1476 1476 match=matchfn, stat=False)
1477 1477 self.ui.write("\n")
1478 1478
1479 1479 class jsonchangeset(changeset_printer):
1480 1480 '''format changeset information.'''
1481 1481
1482 1482 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1483 1483 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1484 1484 self.cache = {}
1485 1485 self._first = True
1486 1486
1487 1487 def close(self):
1488 1488 if not self._first:
1489 1489 self.ui.write("\n]\n")
1490 1490 else:
1491 1491 self.ui.write("[]\n")
1492 1492
1493 1493 def _show(self, ctx, copies, matchfn, props):
1494 1494 '''show a single changeset or file revision'''
1495 1495 rev = ctx.rev()
1496 1496 if rev is None:
1497 1497 jrev = jnode = 'null'
1498 1498 else:
1499 1499 jrev = '%d' % rev
1500 1500 jnode = '"%s"' % hex(ctx.node())
1501 1501 j = encoding.jsonescape
1502 1502
1503 1503 if self._first:
1504 1504 self.ui.write("[\n {")
1505 1505 self._first = False
1506 1506 else:
1507 1507 self.ui.write(",\n {")
1508 1508
1509 1509 if self.ui.quiet:
1510 1510 self.ui.write(('\n "rev": %s') % jrev)
1511 1511 self.ui.write((',\n "node": %s') % jnode)
1512 1512 self.ui.write('\n }')
1513 1513 return
1514 1514
1515 1515 self.ui.write(('\n "rev": %s') % jrev)
1516 1516 self.ui.write((',\n "node": %s') % jnode)
1517 1517 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1518 1518 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1519 1519 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1520 1520 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1521 1521 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1522 1522
1523 1523 self.ui.write((',\n "bookmarks": [%s]') %
1524 1524 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1525 1525 self.ui.write((',\n "tags": [%s]') %
1526 1526 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1527 1527 self.ui.write((',\n "parents": [%s]') %
1528 1528 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1529 1529
1530 1530 if self.ui.debugflag:
1531 1531 if rev is None:
1532 1532 jmanifestnode = 'null'
1533 1533 else:
1534 1534 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1535 1535 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1536 1536
1537 1537 self.ui.write((',\n "extra": {%s}') %
1538 1538 ", ".join('"%s": "%s"' % (j(k), j(v))
1539 1539 for k, v in ctx.extra().items()))
1540 1540
1541 1541 files = ctx.p1().status(ctx)
1542 1542 self.ui.write((',\n "modified": [%s]') %
1543 1543 ", ".join('"%s"' % j(f) for f in files[0]))
1544 1544 self.ui.write((',\n "added": [%s]') %
1545 1545 ", ".join('"%s"' % j(f) for f in files[1]))
1546 1546 self.ui.write((',\n "removed": [%s]') %
1547 1547 ", ".join('"%s"' % j(f) for f in files[2]))
1548 1548
1549 1549 elif self.ui.verbose:
1550 1550 self.ui.write((',\n "files": [%s]') %
1551 1551 ", ".join('"%s"' % j(f) for f in ctx.files()))
1552 1552
1553 1553 if copies:
1554 1554 self.ui.write((',\n "copies": {%s}') %
1555 1555 ", ".join('"%s": "%s"' % (j(k), j(v))
1556 1556 for k, v in copies))
1557 1557
1558 1558 matchfn = self.matchfn
1559 1559 if matchfn:
1560 1560 stat = self.diffopts.get('stat')
1561 1561 diff = self.diffopts.get('patch')
1562 1562 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1563 1563 node, prev = ctx.node(), ctx.p1().node()
1564 1564 if stat:
1565 1565 self.ui.pushbuffer()
1566 1566 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1567 1567 match=matchfn, stat=True)
1568 1568 self.ui.write((',\n "diffstat": "%s"')
1569 1569 % j(self.ui.popbuffer()))
1570 1570 if diff:
1571 1571 self.ui.pushbuffer()
1572 1572 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1573 1573 match=matchfn, stat=False)
1574 1574 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1575 1575
1576 1576 self.ui.write("\n }")
1577 1577
1578 1578 class changeset_templater(changeset_printer):
1579 1579 '''format changeset information.'''
1580 1580
1581 1581 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1582 1582 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1583 1583 self.t = formatter.loadtemplater(ui, 'changeset', (tmpl, mapfile),
1584 1584 cache=templatekw.defaulttempl)
1585 1585 self._counter = itertools.count()
1586 1586 self.cache = {}
1587 1587
1588 1588 # find correct templates for current mode
1589 1589 tmplmodes = [
1590 1590 (True, None),
1591 1591 (self.ui.verbose, 'verbose'),
1592 1592 (self.ui.quiet, 'quiet'),
1593 1593 (self.ui.debugflag, 'debug'),
1594 1594 ]
1595 1595
1596 1596 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1597 1597 'docheader': '', 'docfooter': ''}
1598 1598 for mode, postfix in tmplmodes:
1599 1599 for t in self._parts:
1600 1600 cur = t
1601 1601 if postfix:
1602 1602 cur += "_" + postfix
1603 1603 if mode and cur in self.t:
1604 1604 self._parts[t] = cur
1605 1605
1606 1606 if self._parts['docheader']:
1607 1607 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1608 1608
1609 1609 def close(self):
1610 1610 if self._parts['docfooter']:
1611 1611 if not self.footer:
1612 1612 self.footer = ""
1613 1613 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1614 1614 return super(changeset_templater, self).close()
1615 1615
1616 1616 def _show(self, ctx, copies, matchfn, props):
1617 1617 '''show a single changeset or file revision'''
1618 1618 props = props.copy()
1619 1619 props.update(templatekw.keywords)
1620 1620 props['templ'] = self.t
1621 1621 props['ctx'] = ctx
1622 1622 props['repo'] = self.repo
1623 1623 props['ui'] = self.repo.ui
1624 1624 props['index'] = next(self._counter)
1625 1625 props['revcache'] = {'copies': copies}
1626 1626 props['cache'] = self.cache
1627 1627 props = pycompat.strkwargs(props)
1628 1628
1629 1629 # write header
1630 1630 if self._parts['header']:
1631 1631 h = templater.stringify(self.t(self._parts['header'], **props))
1632 1632 if self.buffered:
1633 1633 self.header[ctx.rev()] = h
1634 1634 else:
1635 1635 if self.lastheader != h:
1636 1636 self.lastheader = h
1637 1637 self.ui.write(h)
1638 1638
1639 1639 # write changeset metadata, then patch if requested
1640 1640 key = self._parts['changeset']
1641 1641 self.ui.write(templater.stringify(self.t(key, **props)))
1642 1642 self.showpatch(ctx, matchfn)
1643 1643
1644 1644 if self._parts['footer']:
1645 1645 if not self.footer:
1646 1646 self.footer = templater.stringify(
1647 1647 self.t(self._parts['footer'], **props))
1648 1648
1649 def gettemplate(ui, tmpl, style):
1649 def _lookuplogtemplate(ui, tmpl, style):
1650 1650 """
1651 1651 Find the template matching the given template spec or style.
1652 1652 """
1653 1653
1654 1654 # ui settings
1655 1655 if not tmpl and not style: # template are stronger than style
1656 1656 tmpl = ui.config('ui', 'logtemplate')
1657 1657 if tmpl:
1658 1658 return templater.unquotestring(tmpl), None
1659 1659 else:
1660 1660 style = util.expandpath(ui.config('ui', 'style', ''))
1661 1661
1662 1662 if not tmpl and style:
1663 1663 mapfile = style
1664 1664 if not os.path.split(mapfile)[0]:
1665 1665 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1666 1666 or templater.templatepath(mapfile))
1667 1667 if mapname:
1668 1668 mapfile = mapname
1669 1669 return None, mapfile
1670 1670
1671 1671 if not tmpl:
1672 1672 return None, None
1673 1673
1674 1674 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1675 1675
1676 1676 def show_changeset(ui, repo, opts, buffered=False):
1677 1677 """show one changeset using template or regular display.
1678 1678
1679 1679 Display format will be the first non-empty hit of:
1680 1680 1. option 'template'
1681 1681 2. option 'style'
1682 1682 3. [ui] setting 'logtemplate'
1683 1683 4. [ui] setting 'style'
1684 1684 If all of these values are either the unset or the empty string,
1685 1685 regular display via changeset_printer() is done.
1686 1686 """
1687 1687 # options
1688 1688 matchfn = None
1689 1689 if opts.get('patch') or opts.get('stat'):
1690 1690 matchfn = scmutil.matchall(repo)
1691 1691
1692 1692 if opts.get('template') == 'json':
1693 1693 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1694 1694
1695 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1695 spec = _lookuplogtemplate(ui, opts.get('template'), opts.get('style'))
1696 tmpl, mapfile = spec
1696 1697
1697 1698 if not tmpl and not mapfile:
1698 1699 return changeset_printer(ui, repo, matchfn, opts, buffered)
1699 1700
1700 1701 return changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile, buffered)
1701 1702
1702 1703 def showmarker(fm, marker, index=None):
1703 1704 """utility function to display obsolescence marker in a readable way
1704 1705
1705 1706 To be used by debug function."""
1706 1707 if index is not None:
1707 1708 fm.write('index', '%i ', index)
1708 1709 fm.write('precnode', '%s ', hex(marker.precnode()))
1709 1710 succs = marker.succnodes()
1710 1711 fm.condwrite(succs, 'succnodes', '%s ',
1711 1712 fm.formatlist(map(hex, succs), name='node'))
1712 1713 fm.write('flag', '%X ', marker.flags())
1713 1714 parents = marker.parentnodes()
1714 1715 if parents is not None:
1715 1716 fm.write('parentnodes', '{%s} ',
1716 1717 fm.formatlist(map(hex, parents), name='node', sep=', '))
1717 1718 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1718 1719 meta = marker.metadata().copy()
1719 1720 meta.pop('date', None)
1720 1721 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
1721 1722 fm.plain('\n')
1722 1723
1723 1724 def finddate(ui, repo, date):
1724 1725 """Find the tipmost changeset that matches the given date spec"""
1725 1726
1726 1727 df = util.matchdate(date)
1727 1728 m = scmutil.matchall(repo)
1728 1729 results = {}
1729 1730
1730 1731 def prep(ctx, fns):
1731 1732 d = ctx.date()
1732 1733 if df(d[0]):
1733 1734 results[ctx.rev()] = d
1734 1735
1735 1736 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1736 1737 rev = ctx.rev()
1737 1738 if rev in results:
1738 1739 ui.status(_("found revision %s from %s\n") %
1739 1740 (rev, util.datestr(results[rev])))
1740 1741 return '%d' % rev
1741 1742
1742 1743 raise error.Abort(_("revision matching date not found"))
1743 1744
1744 1745 def increasingwindows(windowsize=8, sizelimit=512):
1745 1746 while True:
1746 1747 yield windowsize
1747 1748 if windowsize < sizelimit:
1748 1749 windowsize *= 2
1749 1750
1750 1751 class FileWalkError(Exception):
1751 1752 pass
1752 1753
1753 1754 def walkfilerevs(repo, match, follow, revs, fncache):
1754 1755 '''Walks the file history for the matched files.
1755 1756
1756 1757 Returns the changeset revs that are involved in the file history.
1757 1758
1758 1759 Throws FileWalkError if the file history can't be walked using
1759 1760 filelogs alone.
1760 1761 '''
1761 1762 wanted = set()
1762 1763 copies = []
1763 1764 minrev, maxrev = min(revs), max(revs)
1764 1765 def filerevgen(filelog, last):
1765 1766 """
1766 1767 Only files, no patterns. Check the history of each file.
1767 1768
1768 1769 Examines filelog entries within minrev, maxrev linkrev range
1769 1770 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1770 1771 tuples in backwards order
1771 1772 """
1772 1773 cl_count = len(repo)
1773 1774 revs = []
1774 1775 for j in xrange(0, last + 1):
1775 1776 linkrev = filelog.linkrev(j)
1776 1777 if linkrev < minrev:
1777 1778 continue
1778 1779 # only yield rev for which we have the changelog, it can
1779 1780 # happen while doing "hg log" during a pull or commit
1780 1781 if linkrev >= cl_count:
1781 1782 break
1782 1783
1783 1784 parentlinkrevs = []
1784 1785 for p in filelog.parentrevs(j):
1785 1786 if p != nullrev:
1786 1787 parentlinkrevs.append(filelog.linkrev(p))
1787 1788 n = filelog.node(j)
1788 1789 revs.append((linkrev, parentlinkrevs,
1789 1790 follow and filelog.renamed(n)))
1790 1791
1791 1792 return reversed(revs)
1792 1793 def iterfiles():
1793 1794 pctx = repo['.']
1794 1795 for filename in match.files():
1795 1796 if follow:
1796 1797 if filename not in pctx:
1797 1798 raise error.Abort(_('cannot follow file not in parent '
1798 1799 'revision: "%s"') % filename)
1799 1800 yield filename, pctx[filename].filenode()
1800 1801 else:
1801 1802 yield filename, None
1802 1803 for filename_node in copies:
1803 1804 yield filename_node
1804 1805
1805 1806 for file_, node in iterfiles():
1806 1807 filelog = repo.file(file_)
1807 1808 if not len(filelog):
1808 1809 if node is None:
1809 1810 # A zero count may be a directory or deleted file, so
1810 1811 # try to find matching entries on the slow path.
1811 1812 if follow:
1812 1813 raise error.Abort(
1813 1814 _('cannot follow nonexistent file: "%s"') % file_)
1814 1815 raise FileWalkError("Cannot walk via filelog")
1815 1816 else:
1816 1817 continue
1817 1818
1818 1819 if node is None:
1819 1820 last = len(filelog) - 1
1820 1821 else:
1821 1822 last = filelog.rev(node)
1822 1823
1823 1824 # keep track of all ancestors of the file
1824 1825 ancestors = {filelog.linkrev(last)}
1825 1826
1826 1827 # iterate from latest to oldest revision
1827 1828 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1828 1829 if not follow:
1829 1830 if rev > maxrev:
1830 1831 continue
1831 1832 else:
1832 1833 # Note that last might not be the first interesting
1833 1834 # rev to us:
1834 1835 # if the file has been changed after maxrev, we'll
1835 1836 # have linkrev(last) > maxrev, and we still need
1836 1837 # to explore the file graph
1837 1838 if rev not in ancestors:
1838 1839 continue
1839 1840 # XXX insert 1327 fix here
1840 1841 if flparentlinkrevs:
1841 1842 ancestors.update(flparentlinkrevs)
1842 1843
1843 1844 fncache.setdefault(rev, []).append(file_)
1844 1845 wanted.add(rev)
1845 1846 if copied:
1846 1847 copies.append(copied)
1847 1848
1848 1849 return wanted
1849 1850
1850 1851 class _followfilter(object):
1851 1852 def __init__(self, repo, onlyfirst=False):
1852 1853 self.repo = repo
1853 1854 self.startrev = nullrev
1854 1855 self.roots = set()
1855 1856 self.onlyfirst = onlyfirst
1856 1857
1857 1858 def match(self, rev):
1858 1859 def realparents(rev):
1859 1860 if self.onlyfirst:
1860 1861 return self.repo.changelog.parentrevs(rev)[0:1]
1861 1862 else:
1862 1863 return filter(lambda x: x != nullrev,
1863 1864 self.repo.changelog.parentrevs(rev))
1864 1865
1865 1866 if self.startrev == nullrev:
1866 1867 self.startrev = rev
1867 1868 return True
1868 1869
1869 1870 if rev > self.startrev:
1870 1871 # forward: all descendants
1871 1872 if not self.roots:
1872 1873 self.roots.add(self.startrev)
1873 1874 for parent in realparents(rev):
1874 1875 if parent in self.roots:
1875 1876 self.roots.add(rev)
1876 1877 return True
1877 1878 else:
1878 1879 # backwards: all parents
1879 1880 if not self.roots:
1880 1881 self.roots.update(realparents(self.startrev))
1881 1882 if rev in self.roots:
1882 1883 self.roots.remove(rev)
1883 1884 self.roots.update(realparents(rev))
1884 1885 return True
1885 1886
1886 1887 return False
1887 1888
1888 1889 def walkchangerevs(repo, match, opts, prepare):
1889 1890 '''Iterate over files and the revs in which they changed.
1890 1891
1891 1892 Callers most commonly need to iterate backwards over the history
1892 1893 in which they are interested. Doing so has awful (quadratic-looking)
1893 1894 performance, so we use iterators in a "windowed" way.
1894 1895
1895 1896 We walk a window of revisions in the desired order. Within the
1896 1897 window, we first walk forwards to gather data, then in the desired
1897 1898 order (usually backwards) to display it.
1898 1899
1899 1900 This function returns an iterator yielding contexts. Before
1900 1901 yielding each context, the iterator will first call the prepare
1901 1902 function on each context in the window in forward order.'''
1902 1903
1903 1904 follow = opts.get('follow') or opts.get('follow_first')
1904 1905 revs = _logrevs(repo, opts)
1905 1906 if not revs:
1906 1907 return []
1907 1908 wanted = set()
1908 1909 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1909 1910 opts.get('removed'))
1910 1911 fncache = {}
1911 1912 change = repo.changectx
1912 1913
1913 1914 # First step is to fill wanted, the set of revisions that we want to yield.
1914 1915 # When it does not induce extra cost, we also fill fncache for revisions in
1915 1916 # wanted: a cache of filenames that were changed (ctx.files()) and that
1916 1917 # match the file filtering conditions.
1917 1918
1918 1919 if match.always():
1919 1920 # No files, no patterns. Display all revs.
1920 1921 wanted = revs
1921 1922 elif not slowpath:
1922 1923 # We only have to read through the filelog to find wanted revisions
1923 1924
1924 1925 try:
1925 1926 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1926 1927 except FileWalkError:
1927 1928 slowpath = True
1928 1929
1929 1930 # We decided to fall back to the slowpath because at least one
1930 1931 # of the paths was not a file. Check to see if at least one of them
1931 1932 # existed in history, otherwise simply return
1932 1933 for path in match.files():
1933 1934 if path == '.' or path in repo.store:
1934 1935 break
1935 1936 else:
1936 1937 return []
1937 1938
1938 1939 if slowpath:
1939 1940 # We have to read the changelog to match filenames against
1940 1941 # changed files
1941 1942
1942 1943 if follow:
1943 1944 raise error.Abort(_('can only follow copies/renames for explicit '
1944 1945 'filenames'))
1945 1946
1946 1947 # The slow path checks files modified in every changeset.
1947 1948 # This is really slow on large repos, so compute the set lazily.
1948 1949 class lazywantedset(object):
1949 1950 def __init__(self):
1950 1951 self.set = set()
1951 1952 self.revs = set(revs)
1952 1953
1953 1954 # No need to worry about locality here because it will be accessed
1954 1955 # in the same order as the increasing window below.
1955 1956 def __contains__(self, value):
1956 1957 if value in self.set:
1957 1958 return True
1958 1959 elif not value in self.revs:
1959 1960 return False
1960 1961 else:
1961 1962 self.revs.discard(value)
1962 1963 ctx = change(value)
1963 1964 matches = filter(match, ctx.files())
1964 1965 if matches:
1965 1966 fncache[value] = matches
1966 1967 self.set.add(value)
1967 1968 return True
1968 1969 return False
1969 1970
1970 1971 def discard(self, value):
1971 1972 self.revs.discard(value)
1972 1973 self.set.discard(value)
1973 1974
1974 1975 wanted = lazywantedset()
1975 1976
1976 1977 # it might be worthwhile to do this in the iterator if the rev range
1977 1978 # is descending and the prune args are all within that range
1978 1979 for rev in opts.get('prune', ()):
1979 1980 rev = repo[rev].rev()
1980 1981 ff = _followfilter(repo)
1981 1982 stop = min(revs[0], revs[-1])
1982 1983 for x in xrange(rev, stop - 1, -1):
1983 1984 if ff.match(x):
1984 1985 wanted = wanted - [x]
1985 1986
1986 1987 # Now that wanted is correctly initialized, we can iterate over the
1987 1988 # revision range, yielding only revisions in wanted.
1988 1989 def iterate():
1989 1990 if follow and match.always():
1990 1991 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1991 1992 def want(rev):
1992 1993 return ff.match(rev) and rev in wanted
1993 1994 else:
1994 1995 def want(rev):
1995 1996 return rev in wanted
1996 1997
1997 1998 it = iter(revs)
1998 1999 stopiteration = False
1999 2000 for windowsize in increasingwindows():
2000 2001 nrevs = []
2001 2002 for i in xrange(windowsize):
2002 2003 rev = next(it, None)
2003 2004 if rev is None:
2004 2005 stopiteration = True
2005 2006 break
2006 2007 elif want(rev):
2007 2008 nrevs.append(rev)
2008 2009 for rev in sorted(nrevs):
2009 2010 fns = fncache.get(rev)
2010 2011 ctx = change(rev)
2011 2012 if not fns:
2012 2013 def fns_generator():
2013 2014 for f in ctx.files():
2014 2015 if match(f):
2015 2016 yield f
2016 2017 fns = fns_generator()
2017 2018 prepare(ctx, fns)
2018 2019 for rev in nrevs:
2019 2020 yield change(rev)
2020 2021
2021 2022 if stopiteration:
2022 2023 break
2023 2024
2024 2025 return iterate()
2025 2026
2026 2027 def _makefollowlogfilematcher(repo, files, followfirst):
2027 2028 # When displaying a revision with --patch --follow FILE, we have
2028 2029 # to know which file of the revision must be diffed. With
2029 2030 # --follow, we want the names of the ancestors of FILE in the
2030 2031 # revision, stored in "fcache". "fcache" is populated by
2031 2032 # reproducing the graph traversal already done by --follow revset
2032 2033 # and relating revs to file names (which is not "correct" but
2033 2034 # good enough).
2034 2035 fcache = {}
2035 2036 fcacheready = [False]
2036 2037 pctx = repo['.']
2037 2038
2038 2039 def populate():
2039 2040 for fn in files:
2040 2041 fctx = pctx[fn]
2041 2042 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
2042 2043 for c in fctx.ancestors(followfirst=followfirst):
2043 2044 fcache.setdefault(c.rev(), set()).add(c.path())
2044 2045
2045 2046 def filematcher(rev):
2046 2047 if not fcacheready[0]:
2047 2048 # Lazy initialization
2048 2049 fcacheready[0] = True
2049 2050 populate()
2050 2051 return scmutil.matchfiles(repo, fcache.get(rev, []))
2051 2052
2052 2053 return filematcher
2053 2054
2054 2055 def _makenofollowlogfilematcher(repo, pats, opts):
2055 2056 '''hook for extensions to override the filematcher for non-follow cases'''
2056 2057 return None
2057 2058
2058 2059 def _makelogrevset(repo, pats, opts, revs):
2059 2060 """Return (expr, filematcher) where expr is a revset string built
2060 2061 from log options and file patterns or None. If --stat or --patch
2061 2062 are not passed filematcher is None. Otherwise it is a callable
2062 2063 taking a revision number and returning a match objects filtering
2063 2064 the files to be detailed when displaying the revision.
2064 2065 """
2065 2066 opt2revset = {
2066 2067 'no_merges': ('not merge()', None),
2067 2068 'only_merges': ('merge()', None),
2068 2069 '_ancestors': ('ancestors(%(val)s)', None),
2069 2070 '_fancestors': ('_firstancestors(%(val)s)', None),
2070 2071 '_descendants': ('descendants(%(val)s)', None),
2071 2072 '_fdescendants': ('_firstdescendants(%(val)s)', None),
2072 2073 '_matchfiles': ('_matchfiles(%(val)s)', None),
2073 2074 'date': ('date(%(val)r)', None),
2074 2075 'branch': ('branch(%(val)r)', ' or '),
2075 2076 '_patslog': ('filelog(%(val)r)', ' or '),
2076 2077 '_patsfollow': ('follow(%(val)r)', ' or '),
2077 2078 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
2078 2079 'keyword': ('keyword(%(val)r)', ' or '),
2079 2080 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
2080 2081 'user': ('user(%(val)r)', ' or '),
2081 2082 }
2082 2083
2083 2084 opts = dict(opts)
2084 2085 # follow or not follow?
2085 2086 follow = opts.get('follow') or opts.get('follow_first')
2086 2087 if opts.get('follow_first'):
2087 2088 followfirst = 1
2088 2089 else:
2089 2090 followfirst = 0
2090 2091 # --follow with FILE behavior depends on revs...
2091 2092 it = iter(revs)
2092 2093 startrev = next(it)
2093 2094 followdescendants = startrev < next(it, startrev)
2094 2095
2095 2096 # branch and only_branch are really aliases and must be handled at
2096 2097 # the same time
2097 2098 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
2098 2099 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
2099 2100 # pats/include/exclude are passed to match.match() directly in
2100 2101 # _matchfiles() revset but walkchangerevs() builds its matcher with
2101 2102 # scmutil.match(). The difference is input pats are globbed on
2102 2103 # platforms without shell expansion (windows).
2103 2104 wctx = repo[None]
2104 2105 match, pats = scmutil.matchandpats(wctx, pats, opts)
2105 2106 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
2106 2107 opts.get('removed'))
2107 2108 if not slowpath:
2108 2109 for f in match.files():
2109 2110 if follow and f not in wctx:
2110 2111 # If the file exists, it may be a directory, so let it
2111 2112 # take the slow path.
2112 2113 if os.path.exists(repo.wjoin(f)):
2113 2114 slowpath = True
2114 2115 continue
2115 2116 else:
2116 2117 raise error.Abort(_('cannot follow file not in parent '
2117 2118 'revision: "%s"') % f)
2118 2119 filelog = repo.file(f)
2119 2120 if not filelog:
2120 2121 # A zero count may be a directory or deleted file, so
2121 2122 # try to find matching entries on the slow path.
2122 2123 if follow:
2123 2124 raise error.Abort(
2124 2125 _('cannot follow nonexistent file: "%s"') % f)
2125 2126 slowpath = True
2126 2127
2127 2128 # We decided to fall back to the slowpath because at least one
2128 2129 # of the paths was not a file. Check to see if at least one of them
2129 2130 # existed in history - in that case, we'll continue down the
2130 2131 # slowpath; otherwise, we can turn off the slowpath
2131 2132 if slowpath:
2132 2133 for path in match.files():
2133 2134 if path == '.' or path in repo.store:
2134 2135 break
2135 2136 else:
2136 2137 slowpath = False
2137 2138
2138 2139 fpats = ('_patsfollow', '_patsfollowfirst')
2139 2140 fnopats = (('_ancestors', '_fancestors'),
2140 2141 ('_descendants', '_fdescendants'))
2141 2142 if slowpath:
2142 2143 # See walkchangerevs() slow path.
2143 2144 #
2144 2145 # pats/include/exclude cannot be represented as separate
2145 2146 # revset expressions as their filtering logic applies at file
2146 2147 # level. For instance "-I a -X a" matches a revision touching
2147 2148 # "a" and "b" while "file(a) and not file(b)" does
2148 2149 # not. Besides, filesets are evaluated against the working
2149 2150 # directory.
2150 2151 matchargs = ['r:', 'd:relpath']
2151 2152 for p in pats:
2152 2153 matchargs.append('p:' + p)
2153 2154 for p in opts.get('include', []):
2154 2155 matchargs.append('i:' + p)
2155 2156 for p in opts.get('exclude', []):
2156 2157 matchargs.append('x:' + p)
2157 2158 matchargs = ','.join(('%r' % p) for p in matchargs)
2158 2159 opts['_matchfiles'] = matchargs
2159 2160 if follow:
2160 2161 opts[fnopats[0][followfirst]] = '.'
2161 2162 else:
2162 2163 if follow:
2163 2164 if pats:
2164 2165 # follow() revset interprets its file argument as a
2165 2166 # manifest entry, so use match.files(), not pats.
2166 2167 opts[fpats[followfirst]] = list(match.files())
2167 2168 else:
2168 2169 op = fnopats[followdescendants][followfirst]
2169 2170 opts[op] = 'rev(%d)' % startrev
2170 2171 else:
2171 2172 opts['_patslog'] = list(pats)
2172 2173
2173 2174 filematcher = None
2174 2175 if opts.get('patch') or opts.get('stat'):
2175 2176 # When following files, track renames via a special matcher.
2176 2177 # If we're forced to take the slowpath it means we're following
2177 2178 # at least one pattern/directory, so don't bother with rename tracking.
2178 2179 if follow and not match.always() and not slowpath:
2179 2180 # _makefollowlogfilematcher expects its files argument to be
2180 2181 # relative to the repo root, so use match.files(), not pats.
2181 2182 filematcher = _makefollowlogfilematcher(repo, match.files(),
2182 2183 followfirst)
2183 2184 else:
2184 2185 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2185 2186 if filematcher is None:
2186 2187 filematcher = lambda rev: match
2187 2188
2188 2189 expr = []
2189 2190 for op, val in sorted(opts.iteritems()):
2190 2191 if not val:
2191 2192 continue
2192 2193 if op not in opt2revset:
2193 2194 continue
2194 2195 revop, andor = opt2revset[op]
2195 2196 if '%(val)' not in revop:
2196 2197 expr.append(revop)
2197 2198 else:
2198 2199 if not isinstance(val, list):
2199 2200 e = revop % {'val': val}
2200 2201 else:
2201 2202 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2202 2203 expr.append(e)
2203 2204
2204 2205 if expr:
2205 2206 expr = '(' + ' and '.join(expr) + ')'
2206 2207 else:
2207 2208 expr = None
2208 2209 return expr, filematcher
2209 2210
2210 2211 def _logrevs(repo, opts):
2211 2212 # Default --rev value depends on --follow but --follow behavior
2212 2213 # depends on revisions resolved from --rev...
2213 2214 follow = opts.get('follow') or opts.get('follow_first')
2214 2215 if opts.get('rev'):
2215 2216 revs = scmutil.revrange(repo, opts['rev'])
2216 2217 elif follow and repo.dirstate.p1() == nullid:
2217 2218 revs = smartset.baseset()
2218 2219 elif follow:
2219 2220 revs = repo.revs('reverse(:.)')
2220 2221 else:
2221 2222 revs = smartset.spanset(repo)
2222 2223 revs.reverse()
2223 2224 return revs
2224 2225
2225 2226 def getgraphlogrevs(repo, pats, opts):
2226 2227 """Return (revs, expr, filematcher) where revs is an iterable of
2227 2228 revision numbers, expr is a revset string built from log options
2228 2229 and file patterns or None, and used to filter 'revs'. If --stat or
2229 2230 --patch are not passed filematcher is None. Otherwise it is a
2230 2231 callable taking a revision number and returning a match objects
2231 2232 filtering the files to be detailed when displaying the revision.
2232 2233 """
2233 2234 limit = loglimit(opts)
2234 2235 revs = _logrevs(repo, opts)
2235 2236 if not revs:
2236 2237 return smartset.baseset(), None, None
2237 2238 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2238 2239 if opts.get('rev'):
2239 2240 # User-specified revs might be unsorted, but don't sort before
2240 2241 # _makelogrevset because it might depend on the order of revs
2241 2242 if not (revs.isdescending() or revs.istopo()):
2242 2243 revs.sort(reverse=True)
2243 2244 if expr:
2244 2245 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2245 2246 revs = matcher(repo, revs)
2246 2247 if limit is not None:
2247 2248 limitedrevs = []
2248 2249 for idx, rev in enumerate(revs):
2249 2250 if idx >= limit:
2250 2251 break
2251 2252 limitedrevs.append(rev)
2252 2253 revs = smartset.baseset(limitedrevs)
2253 2254
2254 2255 return revs, expr, filematcher
2255 2256
2256 2257 def getlogrevs(repo, pats, opts):
2257 2258 """Return (revs, expr, filematcher) where revs is an iterable of
2258 2259 revision numbers, expr is a revset string built from log options
2259 2260 and file patterns or None, and used to filter 'revs'. If --stat or
2260 2261 --patch are not passed filematcher is None. Otherwise it is a
2261 2262 callable taking a revision number and returning a match objects
2262 2263 filtering the files to be detailed when displaying the revision.
2263 2264 """
2264 2265 limit = loglimit(opts)
2265 2266 revs = _logrevs(repo, opts)
2266 2267 if not revs:
2267 2268 return smartset.baseset([]), None, None
2268 2269 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2269 2270 if expr:
2270 2271 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2271 2272 revs = matcher(repo, revs)
2272 2273 if limit is not None:
2273 2274 limitedrevs = []
2274 2275 for idx, r in enumerate(revs):
2275 2276 if limit <= idx:
2276 2277 break
2277 2278 limitedrevs.append(r)
2278 2279 revs = smartset.baseset(limitedrevs)
2279 2280
2280 2281 return revs, expr, filematcher
2281 2282
2282 2283 def _graphnodeformatter(ui, displayer):
2283 2284 spec = ui.config('ui', 'graphnodetemplate')
2284 2285 if not spec:
2285 2286 return templatekw.showgraphnode # fast path for "{graphnode}"
2286 2287
2287 2288 spec = templater.unquotestring(spec)
2288 2289 templ = formatter.maketemplater(ui, 'graphnode', spec)
2289 2290 cache = {}
2290 2291 if isinstance(displayer, changeset_templater):
2291 2292 cache = displayer.cache # reuse cache of slow templates
2292 2293 props = templatekw.keywords.copy()
2293 2294 props['templ'] = templ
2294 2295 props['cache'] = cache
2295 2296 def formatnode(repo, ctx):
2296 2297 props['ctx'] = ctx
2297 2298 props['repo'] = repo
2298 2299 props['ui'] = repo.ui
2299 2300 props['revcache'] = {}
2300 2301 return templater.stringify(templ('graphnode', **props))
2301 2302 return formatnode
2302 2303
2303 2304 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2304 2305 filematcher=None):
2305 2306 formatnode = _graphnodeformatter(ui, displayer)
2306 2307 state = graphmod.asciistate()
2307 2308 styles = state['styles']
2308 2309
2309 2310 # only set graph styling if HGPLAIN is not set.
2310 2311 if ui.plain('graph'):
2311 2312 # set all edge styles to |, the default pre-3.8 behaviour
2312 2313 styles.update(dict.fromkeys(styles, '|'))
2313 2314 else:
2314 2315 edgetypes = {
2315 2316 'parent': graphmod.PARENT,
2316 2317 'grandparent': graphmod.GRANDPARENT,
2317 2318 'missing': graphmod.MISSINGPARENT
2318 2319 }
2319 2320 for name, key in edgetypes.items():
2320 2321 # experimental config: experimental.graphstyle.*
2321 2322 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2322 2323 styles[key])
2323 2324 if not styles[key]:
2324 2325 styles[key] = None
2325 2326
2326 2327 # experimental config: experimental.graphshorten
2327 2328 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2328 2329
2329 2330 for rev, type, ctx, parents in dag:
2330 2331 char = formatnode(repo, ctx)
2331 2332 copies = None
2332 2333 if getrenamed and ctx.rev():
2333 2334 copies = []
2334 2335 for fn in ctx.files():
2335 2336 rename = getrenamed(fn, ctx.rev())
2336 2337 if rename:
2337 2338 copies.append((fn, rename[0]))
2338 2339 revmatchfn = None
2339 2340 if filematcher is not None:
2340 2341 revmatchfn = filematcher(ctx.rev())
2341 2342 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2342 2343 lines = displayer.hunk.pop(rev).split('\n')
2343 2344 if not lines[-1]:
2344 2345 del lines[-1]
2345 2346 displayer.flush(ctx)
2346 2347 edges = edgefn(type, char, lines, state, rev, parents)
2347 2348 for type, char, lines, coldata in edges:
2348 2349 graphmod.ascii(ui, state, type, char, lines, coldata)
2349 2350 displayer.close()
2350 2351
2351 2352 def graphlog(ui, repo, pats, opts):
2352 2353 # Parameters are identical to log command ones
2353 2354 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2354 2355 revdag = graphmod.dagwalker(repo, revs)
2355 2356
2356 2357 getrenamed = None
2357 2358 if opts.get('copies'):
2358 2359 endrev = None
2359 2360 if opts.get('rev'):
2360 2361 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2361 2362 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2362 2363
2363 2364 ui.pager('log')
2364 2365 displayer = show_changeset(ui, repo, opts, buffered=True)
2365 2366 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2366 2367 filematcher)
2367 2368
2368 2369 def checkunsupportedgraphflags(pats, opts):
2369 2370 for op in ["newest_first"]:
2370 2371 if op in opts and opts[op]:
2371 2372 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2372 2373 % op.replace("_", "-"))
2373 2374
2374 2375 def graphrevs(repo, nodes, opts):
2375 2376 limit = loglimit(opts)
2376 2377 nodes.reverse()
2377 2378 if limit is not None:
2378 2379 nodes = nodes[:limit]
2379 2380 return graphmod.nodes(repo, nodes)
2380 2381
2381 2382 def add(ui, repo, match, prefix, explicitonly, **opts):
2382 2383 join = lambda f: os.path.join(prefix, f)
2383 2384 bad = []
2384 2385
2385 2386 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2386 2387 names = []
2387 2388 wctx = repo[None]
2388 2389 cca = None
2389 2390 abort, warn = scmutil.checkportabilityalert(ui)
2390 2391 if abort or warn:
2391 2392 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2392 2393
2393 2394 badmatch = matchmod.badmatch(match, badfn)
2394 2395 dirstate = repo.dirstate
2395 2396 # We don't want to just call wctx.walk here, since it would return a lot of
2396 2397 # clean files, which we aren't interested in and takes time.
2397 2398 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2398 2399 True, False, full=False)):
2399 2400 exact = match.exact(f)
2400 2401 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2401 2402 if cca:
2402 2403 cca(f)
2403 2404 names.append(f)
2404 2405 if ui.verbose or not exact:
2405 2406 ui.status(_('adding %s\n') % match.rel(f))
2406 2407
2407 2408 for subpath in sorted(wctx.substate):
2408 2409 sub = wctx.sub(subpath)
2409 2410 try:
2410 2411 submatch = matchmod.subdirmatcher(subpath, match)
2411 2412 if opts.get(r'subrepos'):
2412 2413 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2413 2414 else:
2414 2415 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2415 2416 except error.LookupError:
2416 2417 ui.status(_("skipping missing subrepository: %s\n")
2417 2418 % join(subpath))
2418 2419
2419 2420 if not opts.get(r'dry_run'):
2420 2421 rejected = wctx.add(names, prefix)
2421 2422 bad.extend(f for f in rejected if f in match.files())
2422 2423 return bad
2423 2424
2424 2425 def addwebdirpath(repo, serverpath, webconf):
2425 2426 webconf[serverpath] = repo.root
2426 2427 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2427 2428
2428 2429 for r in repo.revs('filelog("path:.hgsub")'):
2429 2430 ctx = repo[r]
2430 2431 for subpath in ctx.substate:
2431 2432 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2432 2433
2433 2434 def forget(ui, repo, match, prefix, explicitonly):
2434 2435 join = lambda f: os.path.join(prefix, f)
2435 2436 bad = []
2436 2437 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2437 2438 wctx = repo[None]
2438 2439 forgot = []
2439 2440
2440 2441 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2441 2442 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2442 2443 if explicitonly:
2443 2444 forget = [f for f in forget if match.exact(f)]
2444 2445
2445 2446 for subpath in sorted(wctx.substate):
2446 2447 sub = wctx.sub(subpath)
2447 2448 try:
2448 2449 submatch = matchmod.subdirmatcher(subpath, match)
2449 2450 subbad, subforgot = sub.forget(submatch, prefix)
2450 2451 bad.extend([subpath + '/' + f for f in subbad])
2451 2452 forgot.extend([subpath + '/' + f for f in subforgot])
2452 2453 except error.LookupError:
2453 2454 ui.status(_("skipping missing subrepository: %s\n")
2454 2455 % join(subpath))
2455 2456
2456 2457 if not explicitonly:
2457 2458 for f in match.files():
2458 2459 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2459 2460 if f not in forgot:
2460 2461 if repo.wvfs.exists(f):
2461 2462 # Don't complain if the exact case match wasn't given.
2462 2463 # But don't do this until after checking 'forgot', so
2463 2464 # that subrepo files aren't normalized, and this op is
2464 2465 # purely from data cached by the status walk above.
2465 2466 if repo.dirstate.normalize(f) in repo.dirstate:
2466 2467 continue
2467 2468 ui.warn(_('not removing %s: '
2468 2469 'file is already untracked\n')
2469 2470 % match.rel(f))
2470 2471 bad.append(f)
2471 2472
2472 2473 for f in forget:
2473 2474 if ui.verbose or not match.exact(f):
2474 2475 ui.status(_('removing %s\n') % match.rel(f))
2475 2476
2476 2477 rejected = wctx.forget(forget, prefix)
2477 2478 bad.extend(f for f in rejected if f in match.files())
2478 2479 forgot.extend(f for f in forget if f not in rejected)
2479 2480 return bad, forgot
2480 2481
2481 2482 def files(ui, ctx, m, fm, fmt, subrepos):
2482 2483 rev = ctx.rev()
2483 2484 ret = 1
2484 2485 ds = ctx.repo().dirstate
2485 2486
2486 2487 for f in ctx.matches(m):
2487 2488 if rev is None and ds[f] == 'r':
2488 2489 continue
2489 2490 fm.startitem()
2490 2491 if ui.verbose:
2491 2492 fc = ctx[f]
2492 2493 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2493 2494 fm.data(abspath=f)
2494 2495 fm.write('path', fmt, m.rel(f))
2495 2496 ret = 0
2496 2497
2497 2498 for subpath in sorted(ctx.substate):
2498 2499 submatch = matchmod.subdirmatcher(subpath, m)
2499 2500 if (subrepos or m.exact(subpath) or any(submatch.files())):
2500 2501 sub = ctx.sub(subpath)
2501 2502 try:
2502 2503 recurse = m.exact(subpath) or subrepos
2503 2504 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2504 2505 ret = 0
2505 2506 except error.LookupError:
2506 2507 ui.status(_("skipping missing subrepository: %s\n")
2507 2508 % m.abs(subpath))
2508 2509
2509 2510 return ret
2510 2511
2511 2512 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2512 2513 join = lambda f: os.path.join(prefix, f)
2513 2514 ret = 0
2514 2515 s = repo.status(match=m, clean=True)
2515 2516 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2516 2517
2517 2518 wctx = repo[None]
2518 2519
2519 2520 if warnings is None:
2520 2521 warnings = []
2521 2522 warn = True
2522 2523 else:
2523 2524 warn = False
2524 2525
2525 2526 subs = sorted(wctx.substate)
2526 2527 total = len(subs)
2527 2528 count = 0
2528 2529 for subpath in subs:
2529 2530 count += 1
2530 2531 submatch = matchmod.subdirmatcher(subpath, m)
2531 2532 if subrepos or m.exact(subpath) or any(submatch.files()):
2532 2533 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2533 2534 sub = wctx.sub(subpath)
2534 2535 try:
2535 2536 if sub.removefiles(submatch, prefix, after, force, subrepos,
2536 2537 warnings):
2537 2538 ret = 1
2538 2539 except error.LookupError:
2539 2540 warnings.append(_("skipping missing subrepository: %s\n")
2540 2541 % join(subpath))
2541 2542 ui.progress(_('searching'), None)
2542 2543
2543 2544 # warn about failure to delete explicit files/dirs
2544 2545 deleteddirs = util.dirs(deleted)
2545 2546 files = m.files()
2546 2547 total = len(files)
2547 2548 count = 0
2548 2549 for f in files:
2549 2550 def insubrepo():
2550 2551 for subpath in wctx.substate:
2551 2552 if f.startswith(subpath + '/'):
2552 2553 return True
2553 2554 return False
2554 2555
2555 2556 count += 1
2556 2557 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2557 2558 isdir = f in deleteddirs or wctx.hasdir(f)
2558 2559 if (f in repo.dirstate or isdir or f == '.'
2559 2560 or insubrepo() or f in subs):
2560 2561 continue
2561 2562
2562 2563 if repo.wvfs.exists(f):
2563 2564 if repo.wvfs.isdir(f):
2564 2565 warnings.append(_('not removing %s: no tracked files\n')
2565 2566 % m.rel(f))
2566 2567 else:
2567 2568 warnings.append(_('not removing %s: file is untracked\n')
2568 2569 % m.rel(f))
2569 2570 # missing files will generate a warning elsewhere
2570 2571 ret = 1
2571 2572 ui.progress(_('deleting'), None)
2572 2573
2573 2574 if force:
2574 2575 list = modified + deleted + clean + added
2575 2576 elif after:
2576 2577 list = deleted
2577 2578 remaining = modified + added + clean
2578 2579 total = len(remaining)
2579 2580 count = 0
2580 2581 for f in remaining:
2581 2582 count += 1
2582 2583 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2583 2584 warnings.append(_('not removing %s: file still exists\n')
2584 2585 % m.rel(f))
2585 2586 ret = 1
2586 2587 ui.progress(_('skipping'), None)
2587 2588 else:
2588 2589 list = deleted + clean
2589 2590 total = len(modified) + len(added)
2590 2591 count = 0
2591 2592 for f in modified:
2592 2593 count += 1
2593 2594 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2594 2595 warnings.append(_('not removing %s: file is modified (use -f'
2595 2596 ' to force removal)\n') % m.rel(f))
2596 2597 ret = 1
2597 2598 for f in added:
2598 2599 count += 1
2599 2600 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2600 2601 warnings.append(_("not removing %s: file has been marked for add"
2601 2602 " (use 'hg forget' to undo add)\n") % m.rel(f))
2602 2603 ret = 1
2603 2604 ui.progress(_('skipping'), None)
2604 2605
2605 2606 list = sorted(list)
2606 2607 total = len(list)
2607 2608 count = 0
2608 2609 for f in list:
2609 2610 count += 1
2610 2611 if ui.verbose or not m.exact(f):
2611 2612 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2612 2613 ui.status(_('removing %s\n') % m.rel(f))
2613 2614 ui.progress(_('deleting'), None)
2614 2615
2615 2616 with repo.wlock():
2616 2617 if not after:
2617 2618 for f in list:
2618 2619 if f in added:
2619 2620 continue # we never unlink added files on remove
2620 2621 repo.wvfs.unlinkpath(f, ignoremissing=True)
2621 2622 repo[None].forget(list)
2622 2623
2623 2624 if warn:
2624 2625 for warning in warnings:
2625 2626 ui.warn(warning)
2626 2627
2627 2628 return ret
2628 2629
2629 2630 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2630 2631 err = 1
2631 2632
2632 2633 def write(path):
2633 2634 filename = None
2634 2635 if fntemplate:
2635 2636 filename = makefilename(repo, fntemplate, ctx.node(),
2636 2637 pathname=os.path.join(prefix, path))
2637 2638 with formatter.maybereopen(basefm, filename, opts) as fm:
2638 2639 data = ctx[path].data()
2639 2640 if opts.get('decode'):
2640 2641 data = repo.wwritedata(path, data)
2641 2642 fm.startitem()
2642 2643 fm.write('data', '%s', data)
2643 2644 fm.data(abspath=path, path=matcher.rel(path))
2644 2645
2645 2646 # Automation often uses hg cat on single files, so special case it
2646 2647 # for performance to avoid the cost of parsing the manifest.
2647 2648 if len(matcher.files()) == 1 and not matcher.anypats():
2648 2649 file = matcher.files()[0]
2649 2650 mfl = repo.manifestlog
2650 2651 mfnode = ctx.manifestnode()
2651 2652 try:
2652 2653 if mfnode and mfl[mfnode].find(file)[0]:
2653 2654 write(file)
2654 2655 return 0
2655 2656 except KeyError:
2656 2657 pass
2657 2658
2658 2659 for abs in ctx.walk(matcher):
2659 2660 write(abs)
2660 2661 err = 0
2661 2662
2662 2663 for subpath in sorted(ctx.substate):
2663 2664 sub = ctx.sub(subpath)
2664 2665 try:
2665 2666 submatch = matchmod.subdirmatcher(subpath, matcher)
2666 2667
2667 2668 if not sub.cat(submatch, basefm, fntemplate,
2668 2669 os.path.join(prefix, sub._path), **opts):
2669 2670 err = 0
2670 2671 except error.RepoLookupError:
2671 2672 ui.status(_("skipping missing subrepository: %s\n")
2672 2673 % os.path.join(prefix, subpath))
2673 2674
2674 2675 return err
2675 2676
2676 2677 def commit(ui, repo, commitfunc, pats, opts):
2677 2678 '''commit the specified files or all outstanding changes'''
2678 2679 date = opts.get('date')
2679 2680 if date:
2680 2681 opts['date'] = util.parsedate(date)
2681 2682 message = logmessage(ui, opts)
2682 2683 matcher = scmutil.match(repo[None], pats, opts)
2683 2684
2684 2685 # extract addremove carefully -- this function can be called from a command
2685 2686 # that doesn't support addremove
2686 2687 if opts.get('addremove'):
2687 2688 if scmutil.addremove(repo, matcher, "", opts) != 0:
2688 2689 raise error.Abort(
2689 2690 _("failed to mark all new/missing files as added/removed"))
2690 2691
2691 2692 return commitfunc(ui, repo, message, matcher, opts)
2692 2693
2693 2694 def samefile(f, ctx1, ctx2):
2694 2695 if f in ctx1.manifest():
2695 2696 a = ctx1.filectx(f)
2696 2697 if f in ctx2.manifest():
2697 2698 b = ctx2.filectx(f)
2698 2699 return (not a.cmp(b)
2699 2700 and a.flags() == b.flags())
2700 2701 else:
2701 2702 return False
2702 2703 else:
2703 2704 return f not in ctx2.manifest()
2704 2705
2705 2706 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2706 2707 # avoid cycle context -> subrepo -> cmdutil
2707 2708 from . import context
2708 2709
2709 2710 # amend will reuse the existing user if not specified, but the obsolete
2710 2711 # marker creation requires that the current user's name is specified.
2711 2712 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2712 2713 ui.username() # raise exception if username not set
2713 2714
2714 2715 ui.note(_('amending changeset %s\n') % old)
2715 2716 base = old.p1()
2716 2717 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2717 2718
2718 2719 wlock = lock = newid = None
2719 2720 try:
2720 2721 wlock = repo.wlock()
2721 2722 lock = repo.lock()
2722 2723 with repo.transaction('amend') as tr:
2723 2724 # See if we got a message from -m or -l, if not, open the editor
2724 2725 # with the message of the changeset to amend
2725 2726 message = logmessage(ui, opts)
2726 2727 # ensure logfile does not conflict with later enforcement of the
2727 2728 # message. potential logfile content has been processed by
2728 2729 # `logmessage` anyway.
2729 2730 opts.pop('logfile')
2730 2731 # First, do a regular commit to record all changes in the working
2731 2732 # directory (if there are any)
2732 2733 ui.callhooks = False
2733 2734 activebookmark = repo._bookmarks.active
2734 2735 try:
2735 2736 repo._bookmarks.active = None
2736 2737 opts['message'] = 'temporary amend commit for %s' % old
2737 2738 node = commit(ui, repo, commitfunc, pats, opts)
2738 2739 finally:
2739 2740 repo._bookmarks.active = activebookmark
2740 2741 repo._bookmarks.recordchange(tr)
2741 2742 ui.callhooks = True
2742 2743 ctx = repo[node]
2743 2744
2744 2745 # Participating changesets:
2745 2746 #
2746 2747 # node/ctx o - new (intermediate) commit that contains changes
2747 2748 # | from working dir to go into amending commit
2748 2749 # | (or a workingctx if there were no changes)
2749 2750 # |
2750 2751 # old o - changeset to amend
2751 2752 # |
2752 2753 # base o - parent of amending changeset
2753 2754
2754 2755 # Update extra dict from amended commit (e.g. to preserve graft
2755 2756 # source)
2756 2757 extra.update(old.extra())
2757 2758
2758 2759 # Also update it from the intermediate commit or from the wctx
2759 2760 extra.update(ctx.extra())
2760 2761
2761 2762 if len(old.parents()) > 1:
2762 2763 # ctx.files() isn't reliable for merges, so fall back to the
2763 2764 # slower repo.status() method
2764 2765 files = set([fn for st in repo.status(base, old)[:3]
2765 2766 for fn in st])
2766 2767 else:
2767 2768 files = set(old.files())
2768 2769
2769 2770 # Second, we use either the commit we just did, or if there were no
2770 2771 # changes the parent of the working directory as the version of the
2771 2772 # files in the final amend commit
2772 2773 if node:
2773 2774 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2774 2775
2775 2776 user = ctx.user()
2776 2777 date = ctx.date()
2777 2778 # Recompute copies (avoid recording a -> b -> a)
2778 2779 copied = copies.pathcopies(base, ctx)
2779 2780 if old.p2:
2780 2781 copied.update(copies.pathcopies(old.p2(), ctx))
2781 2782
2782 2783 # Prune files which were reverted by the updates: if old
2783 2784 # introduced file X and our intermediate commit, node,
2784 2785 # renamed that file, then those two files are the same and
2785 2786 # we can discard X from our list of files. Likewise if X
2786 2787 # was deleted, it's no longer relevant
2787 2788 files.update(ctx.files())
2788 2789 files = [f for f in files if not samefile(f, ctx, base)]
2789 2790
2790 2791 def filectxfn(repo, ctx_, path):
2791 2792 try:
2792 2793 fctx = ctx[path]
2793 2794 flags = fctx.flags()
2794 2795 mctx = context.memfilectx(repo,
2795 2796 fctx.path(), fctx.data(),
2796 2797 islink='l' in flags,
2797 2798 isexec='x' in flags,
2798 2799 copied=copied.get(path))
2799 2800 return mctx
2800 2801 except KeyError:
2801 2802 return None
2802 2803 else:
2803 2804 ui.note(_('copying changeset %s to %s\n') % (old, base))
2804 2805
2805 2806 # Use version of files as in the old cset
2806 2807 def filectxfn(repo, ctx_, path):
2807 2808 try:
2808 2809 return old.filectx(path)
2809 2810 except KeyError:
2810 2811 return None
2811 2812
2812 2813 user = opts.get('user') or old.user()
2813 2814 date = opts.get('date') or old.date()
2814 2815 editform = mergeeditform(old, 'commit.amend')
2815 2816 editor = getcommiteditor(editform=editform, **opts)
2816 2817 if not message:
2817 2818 editor = getcommiteditor(edit=True, editform=editform)
2818 2819 message = old.description()
2819 2820
2820 2821 pureextra = extra.copy()
2821 2822 extra['amend_source'] = old.hex()
2822 2823
2823 2824 new = context.memctx(repo,
2824 2825 parents=[base.node(), old.p2().node()],
2825 2826 text=message,
2826 2827 files=files,
2827 2828 filectxfn=filectxfn,
2828 2829 user=user,
2829 2830 date=date,
2830 2831 extra=extra,
2831 2832 editor=editor)
2832 2833
2833 2834 newdesc = changelog.stripdesc(new.description())
2834 2835 if ((not node)
2835 2836 and newdesc == old.description()
2836 2837 and user == old.user()
2837 2838 and date == old.date()
2838 2839 and pureextra == old.extra()):
2839 2840 # nothing changed. continuing here would create a new node
2840 2841 # anyway because of the amend_source noise.
2841 2842 #
2842 2843 # This not what we expect from amend.
2843 2844 return old.node()
2844 2845
2845 2846 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2846 2847 try:
2847 2848 if opts.get('secret'):
2848 2849 commitphase = 'secret'
2849 2850 else:
2850 2851 commitphase = old.phase()
2851 2852 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2852 2853 newid = repo.commitctx(new)
2853 2854 finally:
2854 2855 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2855 2856 if newid != old.node():
2856 2857 # Reroute the working copy parent to the new changeset
2857 2858 repo.setparents(newid, nullid)
2858 2859
2859 2860 # Move bookmarks from old parent to amend commit
2860 2861 bms = repo.nodebookmarks(old.node())
2861 2862 if bms:
2862 2863 marks = repo._bookmarks
2863 2864 for bm in bms:
2864 2865 ui.debug('moving bookmarks %r from %s to %s\n' %
2865 2866 (marks, old.hex(), hex(newid)))
2866 2867 marks[bm] = newid
2867 2868 marks.recordchange(tr)
2868 2869 #commit the whole amend process
2869 2870 if createmarkers:
2870 2871 # mark the new changeset as successor of the rewritten one
2871 2872 new = repo[newid]
2872 2873 obs = [(old, (new,))]
2873 2874 if node:
2874 2875 obs.append((ctx, ()))
2875 2876
2876 2877 obsolete.createmarkers(repo, obs, operation='amend')
2877 2878 if not createmarkers and newid != old.node():
2878 2879 # Strip the intermediate commit (if there was one) and the amended
2879 2880 # commit
2880 2881 if node:
2881 2882 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2882 2883 ui.note(_('stripping amended changeset %s\n') % old)
2883 2884 repair.strip(ui, repo, old.node(), topic='amend-backup')
2884 2885 finally:
2885 2886 lockmod.release(lock, wlock)
2886 2887 return newid
2887 2888
2888 2889 def commiteditor(repo, ctx, subs, editform=''):
2889 2890 if ctx.description():
2890 2891 return ctx.description()
2891 2892 return commitforceeditor(repo, ctx, subs, editform=editform,
2892 2893 unchangedmessagedetection=True)
2893 2894
2894 2895 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2895 2896 editform='', unchangedmessagedetection=False):
2896 2897 if not extramsg:
2897 2898 extramsg = _("Leave message empty to abort commit.")
2898 2899
2899 2900 forms = [e for e in editform.split('.') if e]
2900 2901 forms.insert(0, 'changeset')
2901 2902 templatetext = None
2902 2903 while forms:
2903 2904 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2904 2905 if tmpl:
2905 2906 tmpl = templater.unquotestring(tmpl)
2906 2907 templatetext = committext = buildcommittemplate(
2907 2908 repo, ctx, subs, extramsg, tmpl)
2908 2909 break
2909 2910 forms.pop()
2910 2911 else:
2911 2912 committext = buildcommittext(repo, ctx, subs, extramsg)
2912 2913
2913 2914 # run editor in the repository root
2914 2915 olddir = pycompat.getcwd()
2915 2916 os.chdir(repo.root)
2916 2917
2917 2918 # make in-memory changes visible to external process
2918 2919 tr = repo.currenttransaction()
2919 2920 repo.dirstate.write(tr)
2920 2921 pending = tr and tr.writepending() and repo.root
2921 2922
2922 2923 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2923 2924 editform=editform, pending=pending,
2924 2925 repopath=repo.path)
2925 2926 text = editortext
2926 2927
2927 2928 # strip away anything below this special string (used for editors that want
2928 2929 # to display the diff)
2929 2930 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2930 2931 if stripbelow:
2931 2932 text = text[:stripbelow.start()]
2932 2933
2933 2934 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2934 2935 os.chdir(olddir)
2935 2936
2936 2937 if finishdesc:
2937 2938 text = finishdesc(text)
2938 2939 if not text.strip():
2939 2940 raise error.Abort(_("empty commit message"))
2940 2941 if unchangedmessagedetection and editortext == templatetext:
2941 2942 raise error.Abort(_("commit message unchanged"))
2942 2943
2943 2944 return text
2944 2945
2945 2946 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2946 2947 ui = repo.ui
2947 tmpl, mapfile = gettemplate(ui, tmpl, None)
2948 tmpl, mapfile = _lookuplogtemplate(ui, tmpl, None)
2948 2949
2949 2950 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2950 2951
2951 2952 for k, v in repo.ui.configitems('committemplate'):
2952 2953 if k != 'changeset':
2953 2954 t.t.cache[k] = v
2954 2955
2955 2956 if not extramsg:
2956 2957 extramsg = '' # ensure that extramsg is string
2957 2958
2958 2959 ui.pushbuffer()
2959 2960 t.show(ctx, extramsg=extramsg)
2960 2961 return ui.popbuffer()
2961 2962
2962 2963 def hgprefix(msg):
2963 2964 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2964 2965
2965 2966 def buildcommittext(repo, ctx, subs, extramsg):
2966 2967 edittext = []
2967 2968 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2968 2969 if ctx.description():
2969 2970 edittext.append(ctx.description())
2970 2971 edittext.append("")
2971 2972 edittext.append("") # Empty line between message and comments.
2972 2973 edittext.append(hgprefix(_("Enter commit message."
2973 2974 " Lines beginning with 'HG:' are removed.")))
2974 2975 edittext.append(hgprefix(extramsg))
2975 2976 edittext.append("HG: --")
2976 2977 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2977 2978 if ctx.p2():
2978 2979 edittext.append(hgprefix(_("branch merge")))
2979 2980 if ctx.branch():
2980 2981 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2981 2982 if bookmarks.isactivewdirparent(repo):
2982 2983 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2983 2984 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2984 2985 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2985 2986 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2986 2987 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2987 2988 if not added and not modified and not removed:
2988 2989 edittext.append(hgprefix(_("no files changed")))
2989 2990 edittext.append("")
2990 2991
2991 2992 return "\n".join(edittext)
2992 2993
2993 2994 def commitstatus(repo, node, branch, bheads=None, opts=None):
2994 2995 if opts is None:
2995 2996 opts = {}
2996 2997 ctx = repo[node]
2997 2998 parents = ctx.parents()
2998 2999
2999 3000 if (not opts.get('amend') and bheads and node not in bheads and not
3000 3001 [x for x in parents if x.node() in bheads and x.branch() == branch]):
3001 3002 repo.ui.status(_('created new head\n'))
3002 3003 # The message is not printed for initial roots. For the other
3003 3004 # changesets, it is printed in the following situations:
3004 3005 #
3005 3006 # Par column: for the 2 parents with ...
3006 3007 # N: null or no parent
3007 3008 # B: parent is on another named branch
3008 3009 # C: parent is a regular non head changeset
3009 3010 # H: parent was a branch head of the current branch
3010 3011 # Msg column: whether we print "created new head" message
3011 3012 # In the following, it is assumed that there already exists some
3012 3013 # initial branch heads of the current branch, otherwise nothing is
3013 3014 # printed anyway.
3014 3015 #
3015 3016 # Par Msg Comment
3016 3017 # N N y additional topo root
3017 3018 #
3018 3019 # B N y additional branch root
3019 3020 # C N y additional topo head
3020 3021 # H N n usual case
3021 3022 #
3022 3023 # B B y weird additional branch root
3023 3024 # C B y branch merge
3024 3025 # H B n merge with named branch
3025 3026 #
3026 3027 # C C y additional head from merge
3027 3028 # C H n merge with a head
3028 3029 #
3029 3030 # H H n head merge: head count decreases
3030 3031
3031 3032 if not opts.get('close_branch'):
3032 3033 for r in parents:
3033 3034 if r.closesbranch() and r.branch() == branch:
3034 3035 repo.ui.status(_('reopening closed branch head %d\n') % r)
3035 3036
3036 3037 if repo.ui.debugflag:
3037 3038 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
3038 3039 elif repo.ui.verbose:
3039 3040 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
3040 3041
3041 3042 def postcommitstatus(repo, pats, opts):
3042 3043 return repo.status(match=scmutil.match(repo[None], pats, opts))
3043 3044
3044 3045 def revert(ui, repo, ctx, parents, *pats, **opts):
3045 3046 parent, p2 = parents
3046 3047 node = ctx.node()
3047 3048
3048 3049 mf = ctx.manifest()
3049 3050 if node == p2:
3050 3051 parent = p2
3051 3052
3052 3053 # need all matching names in dirstate and manifest of target rev,
3053 3054 # so have to walk both. do not print errors if files exist in one
3054 3055 # but not other. in both cases, filesets should be evaluated against
3055 3056 # workingctx to get consistent result (issue4497). this means 'set:**'
3056 3057 # cannot be used to select missing files from target rev.
3057 3058
3058 3059 # `names` is a mapping for all elements in working copy and target revision
3059 3060 # The mapping is in the form:
3060 3061 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3061 3062 names = {}
3062 3063
3063 3064 with repo.wlock():
3064 3065 ## filling of the `names` mapping
3065 3066 # walk dirstate to fill `names`
3066 3067
3067 3068 interactive = opts.get('interactive', False)
3068 3069 wctx = repo[None]
3069 3070 m = scmutil.match(wctx, pats, opts)
3070 3071
3071 3072 # we'll need this later
3072 3073 targetsubs = sorted(s for s in wctx.substate if m(s))
3073 3074
3074 3075 if not m.always():
3075 3076 matcher = matchmod.badmatch(m, lambda x, y: False)
3076 3077 for abs in wctx.walk(matcher):
3077 3078 names[abs] = m.rel(abs), m.exact(abs)
3078 3079
3079 3080 # walk target manifest to fill `names`
3080 3081
3081 3082 def badfn(path, msg):
3082 3083 if path in names:
3083 3084 return
3084 3085 if path in ctx.substate:
3085 3086 return
3086 3087 path_ = path + '/'
3087 3088 for f in names:
3088 3089 if f.startswith(path_):
3089 3090 return
3090 3091 ui.warn("%s: %s\n" % (m.rel(path), msg))
3091 3092
3092 3093 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3093 3094 if abs not in names:
3094 3095 names[abs] = m.rel(abs), m.exact(abs)
3095 3096
3096 3097 # Find status of all file in `names`.
3097 3098 m = scmutil.matchfiles(repo, names)
3098 3099
3099 3100 changes = repo.status(node1=node, match=m,
3100 3101 unknown=True, ignored=True, clean=True)
3101 3102 else:
3102 3103 changes = repo.status(node1=node, match=m)
3103 3104 for kind in changes:
3104 3105 for abs in kind:
3105 3106 names[abs] = m.rel(abs), m.exact(abs)
3106 3107
3107 3108 m = scmutil.matchfiles(repo, names)
3108 3109
3109 3110 modified = set(changes.modified)
3110 3111 added = set(changes.added)
3111 3112 removed = set(changes.removed)
3112 3113 _deleted = set(changes.deleted)
3113 3114 unknown = set(changes.unknown)
3114 3115 unknown.update(changes.ignored)
3115 3116 clean = set(changes.clean)
3116 3117 modadded = set()
3117 3118
3118 3119 # We need to account for the state of the file in the dirstate,
3119 3120 # even when we revert against something else than parent. This will
3120 3121 # slightly alter the behavior of revert (doing back up or not, delete
3121 3122 # or just forget etc).
3122 3123 if parent == node:
3123 3124 dsmodified = modified
3124 3125 dsadded = added
3125 3126 dsremoved = removed
3126 3127 # store all local modifications, useful later for rename detection
3127 3128 localchanges = dsmodified | dsadded
3128 3129 modified, added, removed = set(), set(), set()
3129 3130 else:
3130 3131 changes = repo.status(node1=parent, match=m)
3131 3132 dsmodified = set(changes.modified)
3132 3133 dsadded = set(changes.added)
3133 3134 dsremoved = set(changes.removed)
3134 3135 # store all local modifications, useful later for rename detection
3135 3136 localchanges = dsmodified | dsadded
3136 3137
3137 3138 # only take into account for removes between wc and target
3138 3139 clean |= dsremoved - removed
3139 3140 dsremoved &= removed
3140 3141 # distinct between dirstate remove and other
3141 3142 removed -= dsremoved
3142 3143
3143 3144 modadded = added & dsmodified
3144 3145 added -= modadded
3145 3146
3146 3147 # tell newly modified apart.
3147 3148 dsmodified &= modified
3148 3149 dsmodified |= modified & dsadded # dirstate added may need backup
3149 3150 modified -= dsmodified
3150 3151
3151 3152 # We need to wait for some post-processing to update this set
3152 3153 # before making the distinction. The dirstate will be used for
3153 3154 # that purpose.
3154 3155 dsadded = added
3155 3156
3156 3157 # in case of merge, files that are actually added can be reported as
3157 3158 # modified, we need to post process the result
3158 3159 if p2 != nullid:
3159 3160 mergeadd = set(dsmodified)
3160 3161 for path in dsmodified:
3161 3162 if path in mf:
3162 3163 mergeadd.remove(path)
3163 3164 dsadded |= mergeadd
3164 3165 dsmodified -= mergeadd
3165 3166
3166 3167 # if f is a rename, update `names` to also revert the source
3167 3168 cwd = repo.getcwd()
3168 3169 for f in localchanges:
3169 3170 src = repo.dirstate.copied(f)
3170 3171 # XXX should we check for rename down to target node?
3171 3172 if src and src not in names and repo.dirstate[src] == 'r':
3172 3173 dsremoved.add(src)
3173 3174 names[src] = (repo.pathto(src, cwd), True)
3174 3175
3175 3176 # determine the exact nature of the deleted changesets
3176 3177 deladded = set(_deleted)
3177 3178 for path in _deleted:
3178 3179 if path in mf:
3179 3180 deladded.remove(path)
3180 3181 deleted = _deleted - deladded
3181 3182
3182 3183 # distinguish between file to forget and the other
3183 3184 added = set()
3184 3185 for abs in dsadded:
3185 3186 if repo.dirstate[abs] != 'a':
3186 3187 added.add(abs)
3187 3188 dsadded -= added
3188 3189
3189 3190 for abs in deladded:
3190 3191 if repo.dirstate[abs] == 'a':
3191 3192 dsadded.add(abs)
3192 3193 deladded -= dsadded
3193 3194
3194 3195 # For files marked as removed, we check if an unknown file is present at
3195 3196 # the same path. If a such file exists it may need to be backed up.
3196 3197 # Making the distinction at this stage helps have simpler backup
3197 3198 # logic.
3198 3199 removunk = set()
3199 3200 for abs in removed:
3200 3201 target = repo.wjoin(abs)
3201 3202 if os.path.lexists(target):
3202 3203 removunk.add(abs)
3203 3204 removed -= removunk
3204 3205
3205 3206 dsremovunk = set()
3206 3207 for abs in dsremoved:
3207 3208 target = repo.wjoin(abs)
3208 3209 if os.path.lexists(target):
3209 3210 dsremovunk.add(abs)
3210 3211 dsremoved -= dsremovunk
3211 3212
3212 3213 # action to be actually performed by revert
3213 3214 # (<list of file>, message>) tuple
3214 3215 actions = {'revert': ([], _('reverting %s\n')),
3215 3216 'add': ([], _('adding %s\n')),
3216 3217 'remove': ([], _('removing %s\n')),
3217 3218 'drop': ([], _('removing %s\n')),
3218 3219 'forget': ([], _('forgetting %s\n')),
3219 3220 'undelete': ([], _('undeleting %s\n')),
3220 3221 'noop': (None, _('no changes needed to %s\n')),
3221 3222 'unknown': (None, _('file not managed: %s\n')),
3222 3223 }
3223 3224
3224 3225 # "constant" that convey the backup strategy.
3225 3226 # All set to `discard` if `no-backup` is set do avoid checking
3226 3227 # no_backup lower in the code.
3227 3228 # These values are ordered for comparison purposes
3228 3229 backupinteractive = 3 # do backup if interactively modified
3229 3230 backup = 2 # unconditionally do backup
3230 3231 check = 1 # check if the existing file differs from target
3231 3232 discard = 0 # never do backup
3232 3233 if opts.get('no_backup'):
3233 3234 backupinteractive = backup = check = discard
3234 3235 if interactive:
3235 3236 dsmodifiedbackup = backupinteractive
3236 3237 else:
3237 3238 dsmodifiedbackup = backup
3238 3239 tobackup = set()
3239 3240
3240 3241 backupanddel = actions['remove']
3241 3242 if not opts.get('no_backup'):
3242 3243 backupanddel = actions['drop']
3243 3244
3244 3245 disptable = (
3245 3246 # dispatch table:
3246 3247 # file state
3247 3248 # action
3248 3249 # make backup
3249 3250
3250 3251 ## Sets that results that will change file on disk
3251 3252 # Modified compared to target, no local change
3252 3253 (modified, actions['revert'], discard),
3253 3254 # Modified compared to target, but local file is deleted
3254 3255 (deleted, actions['revert'], discard),
3255 3256 # Modified compared to target, local change
3256 3257 (dsmodified, actions['revert'], dsmodifiedbackup),
3257 3258 # Added since target
3258 3259 (added, actions['remove'], discard),
3259 3260 # Added in working directory
3260 3261 (dsadded, actions['forget'], discard),
3261 3262 # Added since target, have local modification
3262 3263 (modadded, backupanddel, backup),
3263 3264 # Added since target but file is missing in working directory
3264 3265 (deladded, actions['drop'], discard),
3265 3266 # Removed since target, before working copy parent
3266 3267 (removed, actions['add'], discard),
3267 3268 # Same as `removed` but an unknown file exists at the same path
3268 3269 (removunk, actions['add'], check),
3269 3270 # Removed since targe, marked as such in working copy parent
3270 3271 (dsremoved, actions['undelete'], discard),
3271 3272 # Same as `dsremoved` but an unknown file exists at the same path
3272 3273 (dsremovunk, actions['undelete'], check),
3273 3274 ## the following sets does not result in any file changes
3274 3275 # File with no modification
3275 3276 (clean, actions['noop'], discard),
3276 3277 # Existing file, not tracked anywhere
3277 3278 (unknown, actions['unknown'], discard),
3278 3279 )
3279 3280
3280 3281 for abs, (rel, exact) in sorted(names.items()):
3281 3282 # target file to be touch on disk (relative to cwd)
3282 3283 target = repo.wjoin(abs)
3283 3284 # search the entry in the dispatch table.
3284 3285 # if the file is in any of these sets, it was touched in the working
3285 3286 # directory parent and we are sure it needs to be reverted.
3286 3287 for table, (xlist, msg), dobackup in disptable:
3287 3288 if abs not in table:
3288 3289 continue
3289 3290 if xlist is not None:
3290 3291 xlist.append(abs)
3291 3292 if dobackup:
3292 3293 # If in interactive mode, don't automatically create
3293 3294 # .orig files (issue4793)
3294 3295 if dobackup == backupinteractive:
3295 3296 tobackup.add(abs)
3296 3297 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3297 3298 bakname = scmutil.origpath(ui, repo, rel)
3298 3299 ui.note(_('saving current version of %s as %s\n') %
3299 3300 (rel, bakname))
3300 3301 if not opts.get('dry_run'):
3301 3302 if interactive:
3302 3303 util.copyfile(target, bakname)
3303 3304 else:
3304 3305 util.rename(target, bakname)
3305 3306 if ui.verbose or not exact:
3306 3307 if not isinstance(msg, basestring):
3307 3308 msg = msg(abs)
3308 3309 ui.status(msg % rel)
3309 3310 elif exact:
3310 3311 ui.warn(msg % rel)
3311 3312 break
3312 3313
3313 3314 if not opts.get('dry_run'):
3314 3315 needdata = ('revert', 'add', 'undelete')
3315 3316 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3316 3317 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3317 3318
3318 3319 if targetsubs:
3319 3320 # Revert the subrepos on the revert list
3320 3321 for sub in targetsubs:
3321 3322 try:
3322 3323 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3323 3324 except KeyError:
3324 3325 raise error.Abort("subrepository '%s' does not exist in %s!"
3325 3326 % (sub, short(ctx.node())))
3326 3327
3327 3328 def _revertprefetch(repo, ctx, *files):
3328 3329 """Let extension changing the storage layer prefetch content"""
3329 3330 pass
3330 3331
3331 3332 def _performrevert(repo, parents, ctx, actions, interactive=False,
3332 3333 tobackup=None):
3333 3334 """function that actually perform all the actions computed for revert
3334 3335
3335 3336 This is an independent function to let extension to plug in and react to
3336 3337 the imminent revert.
3337 3338
3338 3339 Make sure you have the working directory locked when calling this function.
3339 3340 """
3340 3341 parent, p2 = parents
3341 3342 node = ctx.node()
3342 3343 excluded_files = []
3343 3344 matcher_opts = {"exclude": excluded_files}
3344 3345
3345 3346 def checkout(f):
3346 3347 fc = ctx[f]
3347 3348 repo.wwrite(f, fc.data(), fc.flags())
3348 3349
3349 3350 def doremove(f):
3350 3351 try:
3351 3352 repo.wvfs.unlinkpath(f)
3352 3353 except OSError:
3353 3354 pass
3354 3355 repo.dirstate.remove(f)
3355 3356
3356 3357 audit_path = pathutil.pathauditor(repo.root)
3357 3358 for f in actions['forget'][0]:
3358 3359 if interactive:
3359 3360 choice = repo.ui.promptchoice(
3360 3361 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3361 3362 if choice == 0:
3362 3363 repo.dirstate.drop(f)
3363 3364 else:
3364 3365 excluded_files.append(repo.wjoin(f))
3365 3366 else:
3366 3367 repo.dirstate.drop(f)
3367 3368 for f in actions['remove'][0]:
3368 3369 audit_path(f)
3369 3370 if interactive:
3370 3371 choice = repo.ui.promptchoice(
3371 3372 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3372 3373 if choice == 0:
3373 3374 doremove(f)
3374 3375 else:
3375 3376 excluded_files.append(repo.wjoin(f))
3376 3377 else:
3377 3378 doremove(f)
3378 3379 for f in actions['drop'][0]:
3379 3380 audit_path(f)
3380 3381 repo.dirstate.remove(f)
3381 3382
3382 3383 normal = None
3383 3384 if node == parent:
3384 3385 # We're reverting to our parent. If possible, we'd like status
3385 3386 # to report the file as clean. We have to use normallookup for
3386 3387 # merges to avoid losing information about merged/dirty files.
3387 3388 if p2 != nullid:
3388 3389 normal = repo.dirstate.normallookup
3389 3390 else:
3390 3391 normal = repo.dirstate.normal
3391 3392
3392 3393 newlyaddedandmodifiedfiles = set()
3393 3394 if interactive:
3394 3395 # Prompt the user for changes to revert
3395 3396 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3396 3397 m = scmutil.match(ctx, torevert, matcher_opts)
3397 3398 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3398 3399 diffopts.nodates = True
3399 3400 diffopts.git = True
3400 3401 operation = 'discard'
3401 3402 reversehunks = True
3402 3403 if node != parent:
3403 3404 operation = 'revert'
3404 3405 reversehunks = repo.ui.configbool('experimental',
3405 3406 'revertalternateinteractivemode',
3406 3407 True)
3407 3408 if reversehunks:
3408 3409 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3409 3410 else:
3410 3411 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3411 3412 originalchunks = patch.parsepatch(diff)
3412 3413
3413 3414 try:
3414 3415
3415 3416 chunks, opts = recordfilter(repo.ui, originalchunks,
3416 3417 operation=operation)
3417 3418 if reversehunks:
3418 3419 chunks = patch.reversehunks(chunks)
3419 3420
3420 3421 except patch.PatchError as err:
3421 3422 raise error.Abort(_('error parsing patch: %s') % err)
3422 3423
3423 3424 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3424 3425 if tobackup is None:
3425 3426 tobackup = set()
3426 3427 # Apply changes
3427 3428 fp = stringio()
3428 3429 for c in chunks:
3429 3430 # Create a backup file only if this hunk should be backed up
3430 3431 if ishunk(c) and c.header.filename() in tobackup:
3431 3432 abs = c.header.filename()
3432 3433 target = repo.wjoin(abs)
3433 3434 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3434 3435 util.copyfile(target, bakname)
3435 3436 tobackup.remove(abs)
3436 3437 c.write(fp)
3437 3438 dopatch = fp.tell()
3438 3439 fp.seek(0)
3439 3440 if dopatch:
3440 3441 try:
3441 3442 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3442 3443 except patch.PatchError as err:
3443 3444 raise error.Abort(str(err))
3444 3445 del fp
3445 3446 else:
3446 3447 for f in actions['revert'][0]:
3447 3448 checkout(f)
3448 3449 if normal:
3449 3450 normal(f)
3450 3451
3451 3452 for f in actions['add'][0]:
3452 3453 # Don't checkout modified files, they are already created by the diff
3453 3454 if f not in newlyaddedandmodifiedfiles:
3454 3455 checkout(f)
3455 3456 repo.dirstate.add(f)
3456 3457
3457 3458 normal = repo.dirstate.normallookup
3458 3459 if node == parent and p2 == nullid:
3459 3460 normal = repo.dirstate.normal
3460 3461 for f in actions['undelete'][0]:
3461 3462 checkout(f)
3462 3463 normal(f)
3463 3464
3464 3465 copied = copies.pathcopies(repo[parent], ctx)
3465 3466
3466 3467 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3467 3468 if f in copied:
3468 3469 repo.dirstate.copy(copied[f], f)
3469 3470
3470 3471 class command(registrar.command):
3471 3472 def _doregister(self, func, name, *args, **kwargs):
3472 3473 func._deprecatedregistrar = True # flag for deprecwarn in extensions.py
3473 3474 return super(command, self)._doregister(func, name, *args, **kwargs)
3474 3475
3475 3476 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3476 3477 # commands.outgoing. "missing" is "missing" of the result of
3477 3478 # "findcommonoutgoing()"
3478 3479 outgoinghooks = util.hooks()
3479 3480
3480 3481 # a list of (ui, repo) functions called by commands.summary
3481 3482 summaryhooks = util.hooks()
3482 3483
3483 3484 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3484 3485 #
3485 3486 # functions should return tuple of booleans below, if 'changes' is None:
3486 3487 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3487 3488 #
3488 3489 # otherwise, 'changes' is a tuple of tuples below:
3489 3490 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3490 3491 # - (desturl, destbranch, destpeer, outgoing)
3491 3492 summaryremotehooks = util.hooks()
3492 3493
3493 3494 # A list of state files kept by multistep operations like graft.
3494 3495 # Since graft cannot be aborted, it is considered 'clearable' by update.
3495 3496 # note: bisect is intentionally excluded
3496 3497 # (state file, clearable, allowcommit, error, hint)
3497 3498 unfinishedstates = [
3498 3499 ('graftstate', True, False, _('graft in progress'),
3499 3500 _("use 'hg graft --continue' or 'hg update' to abort")),
3500 3501 ('updatestate', True, False, _('last update was interrupted'),
3501 3502 _("use 'hg update' to get a consistent checkout"))
3502 3503 ]
3503 3504
3504 3505 def checkunfinished(repo, commit=False):
3505 3506 '''Look for an unfinished multistep operation, like graft, and abort
3506 3507 if found. It's probably good to check this right before
3507 3508 bailifchanged().
3508 3509 '''
3509 3510 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3510 3511 if commit and allowcommit:
3511 3512 continue
3512 3513 if repo.vfs.exists(f):
3513 3514 raise error.Abort(msg, hint=hint)
3514 3515
3515 3516 def clearunfinished(repo):
3516 3517 '''Check for unfinished operations (as above), and clear the ones
3517 3518 that are clearable.
3518 3519 '''
3519 3520 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3520 3521 if not clearable and repo.vfs.exists(f):
3521 3522 raise error.Abort(msg, hint=hint)
3522 3523 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3523 3524 if clearable and repo.vfs.exists(f):
3524 3525 util.unlink(repo.vfs.join(f))
3525 3526
3526 3527 afterresolvedstates = [
3527 3528 ('graftstate',
3528 3529 _('hg graft --continue')),
3529 3530 ]
3530 3531
3531 3532 def howtocontinue(repo):
3532 3533 '''Check for an unfinished operation and return the command to finish
3533 3534 it.
3534 3535
3535 3536 afterresolvedstates tuples define a .hg/{file} and the corresponding
3536 3537 command needed to finish it.
3537 3538
3538 3539 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3539 3540 a boolean.
3540 3541 '''
3541 3542 contmsg = _("continue: %s")
3542 3543 for f, msg in afterresolvedstates:
3543 3544 if repo.vfs.exists(f):
3544 3545 return contmsg % msg, True
3545 3546 workingctx = repo[None]
3546 3547 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3547 3548 for s in workingctx.substate)
3548 3549 if dirty:
3549 3550 return contmsg % _("hg commit"), False
3550 3551 return None, None
3551 3552
3552 3553 def checkafterresolved(repo):
3553 3554 '''Inform the user about the next action after completing hg resolve
3554 3555
3555 3556 If there's a matching afterresolvedstates, howtocontinue will yield
3556 3557 repo.ui.warn as the reporter.
3557 3558
3558 3559 Otherwise, it will yield repo.ui.note.
3559 3560 '''
3560 3561 msg, warning = howtocontinue(repo)
3561 3562 if msg is not None:
3562 3563 if warning:
3563 3564 repo.ui.warn("%s\n" % msg)
3564 3565 else:
3565 3566 repo.ui.note("%s\n" % msg)
3566 3567
3567 3568 def wrongtooltocontinue(repo, task):
3568 3569 '''Raise an abort suggesting how to properly continue if there is an
3569 3570 active task.
3570 3571
3571 3572 Uses howtocontinue() to find the active task.
3572 3573
3573 3574 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3574 3575 a hint.
3575 3576 '''
3576 3577 after = howtocontinue(repo)
3577 3578 hint = None
3578 3579 if after[1]:
3579 3580 hint = after[0]
3580 3581 raise error.Abort(_('no %s in progress') % task, hint=hint)
General Comments 0
You need to be logged in to leave comments. Login now