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