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