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