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