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