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