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