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