##// END OF EJS Templates
import: get rid of ui.backupconfig
Jun Wu -
r31457:707f9fd2 default
parent child Browse files
Show More
@@ -1,3471 +1,3469 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 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 allowemptyback = repo.ui.backupconfig('ui', 'allowemptycommit')
975 974 extra = {}
976 975 for idfunc in extrapreimport:
977 976 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
978 try:
977 overrides = {}
979 978 if partial:
980 repo.ui.setconfig('ui', 'allowemptycommit', True)
979 overrides[('ui', 'allowemptycommit')] = True
980 with repo.ui.configoverride(overrides, 'import'):
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 finally:
987 repo.ui.restoreconfig(allowemptyback)
988 986 else:
989 987 if opts.get('exact') or importbranch:
990 988 branch = branch or 'default'
991 989 else:
992 990 branch = p1.branch()
993 991 store = patch.filestore()
994 992 try:
995 993 files = set()
996 994 try:
997 995 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
998 996 files, eolmode=None)
999 997 except patch.PatchError as e:
1000 998 raise error.Abort(str(e))
1001 999 if opts.get('exact'):
1002 1000 editor = None
1003 1001 else:
1004 1002 editor = getcommiteditor(editform='import.bypass')
1005 1003 memctx = context.makememctx(repo, (p1.node(), p2.node()),
1006 1004 message,
1007 1005 user,
1008 1006 date,
1009 1007 branch, files, store,
1010 1008 editor=editor)
1011 1009 n = memctx.commit()
1012 1010 finally:
1013 1011 store.close()
1014 1012 if opts.get('exact') and nocommit:
1015 1013 # --exact with --no-commit is still useful in that it does merge
1016 1014 # and branch bits
1017 1015 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1018 1016 elif opts.get('exact') and hex(n) != nodeid:
1019 1017 raise error.Abort(_('patch is damaged or loses information'))
1020 1018 msg = _('applied to working directory')
1021 1019 if n:
1022 1020 # i18n: refers to a short changeset id
1023 1021 msg = _('created %s') % short(n)
1024 1022 return (msg, n, rejects)
1025 1023 finally:
1026 1024 os.unlink(tmpname)
1027 1025
1028 1026 # facility to let extensions include additional data in an exported patch
1029 1027 # list of identifiers to be executed in order
1030 1028 extraexport = []
1031 1029 # mapping from identifier to actual export function
1032 1030 # function as to return a string to be added to the header or None
1033 1031 # it is given two arguments (sequencenumber, changectx)
1034 1032 extraexportmap = {}
1035 1033
1036 1034 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1037 1035 opts=None, match=None):
1038 1036 '''export changesets as hg patches.'''
1039 1037
1040 1038 total = len(revs)
1041 1039 revwidth = max([len(str(rev)) for rev in revs])
1042 1040 filemode = {}
1043 1041
1044 1042 def single(rev, seqno, fp):
1045 1043 ctx = repo[rev]
1046 1044 node = ctx.node()
1047 1045 parents = [p.node() for p in ctx.parents() if p]
1048 1046 branch = ctx.branch()
1049 1047 if switch_parent:
1050 1048 parents.reverse()
1051 1049
1052 1050 if parents:
1053 1051 prev = parents[0]
1054 1052 else:
1055 1053 prev = nullid
1056 1054
1057 1055 shouldclose = False
1058 1056 if not fp and len(template) > 0:
1059 1057 desc_lines = ctx.description().rstrip().split('\n')
1060 1058 desc = desc_lines[0] #Commit always has a first line.
1061 1059 fp = makefileobj(repo, template, node, desc=desc, total=total,
1062 1060 seqno=seqno, revwidth=revwidth, mode='wb',
1063 1061 modemap=filemode)
1064 1062 shouldclose = True
1065 1063 if fp and not getattr(fp, 'name', '<unnamed>').startswith('<'):
1066 1064 repo.ui.note("%s\n" % fp.name)
1067 1065
1068 1066 if not fp:
1069 1067 write = repo.ui.write
1070 1068 else:
1071 1069 def write(s, **kw):
1072 1070 fp.write(s)
1073 1071
1074 1072 write("# HG changeset patch\n")
1075 1073 write("# User %s\n" % ctx.user())
1076 1074 write("# Date %d %d\n" % ctx.date())
1077 1075 write("# %s\n" % util.datestr(ctx.date()))
1078 1076 if branch and branch != 'default':
1079 1077 write("# Branch %s\n" % branch)
1080 1078 write("# Node ID %s\n" % hex(node))
1081 1079 write("# Parent %s\n" % hex(prev))
1082 1080 if len(parents) > 1:
1083 1081 write("# Parent %s\n" % hex(parents[1]))
1084 1082
1085 1083 for headerid in extraexport:
1086 1084 header = extraexportmap[headerid](seqno, ctx)
1087 1085 if header is not None:
1088 1086 write('# %s\n' % header)
1089 1087 write(ctx.description().rstrip())
1090 1088 write("\n\n")
1091 1089
1092 1090 for chunk, label in patch.diffui(repo, prev, node, match, opts=opts):
1093 1091 write(chunk, label=label)
1094 1092
1095 1093 if shouldclose:
1096 1094 fp.close()
1097 1095
1098 1096 for seqno, rev in enumerate(revs):
1099 1097 single(rev, seqno + 1, fp)
1100 1098
1101 1099 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1102 1100 changes=None, stat=False, fp=None, prefix='',
1103 1101 root='', listsubrepos=False):
1104 1102 '''show diff or diffstat.'''
1105 1103 if fp is None:
1106 1104 write = ui.write
1107 1105 else:
1108 1106 def write(s, **kw):
1109 1107 fp.write(s)
1110 1108
1111 1109 if root:
1112 1110 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1113 1111 else:
1114 1112 relroot = ''
1115 1113 if relroot != '':
1116 1114 # XXX relative roots currently don't work if the root is within a
1117 1115 # subrepo
1118 1116 uirelroot = match.uipath(relroot)
1119 1117 relroot += '/'
1120 1118 for matchroot in match.files():
1121 1119 if not matchroot.startswith(relroot):
1122 1120 ui.warn(_('warning: %s not inside relative root %s\n') % (
1123 1121 match.uipath(matchroot), uirelroot))
1124 1122
1125 1123 if stat:
1126 1124 diffopts = diffopts.copy(context=0)
1127 1125 width = 80
1128 1126 if not ui.plain():
1129 1127 width = ui.termwidth()
1130 1128 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1131 1129 prefix=prefix, relroot=relroot)
1132 1130 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1133 1131 width=width):
1134 1132 write(chunk, label=label)
1135 1133 else:
1136 1134 for chunk, label in patch.diffui(repo, node1, node2, match,
1137 1135 changes, diffopts, prefix=prefix,
1138 1136 relroot=relroot):
1139 1137 write(chunk, label=label)
1140 1138
1141 1139 if listsubrepos:
1142 1140 ctx1 = repo[node1]
1143 1141 ctx2 = repo[node2]
1144 1142 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1145 1143 tempnode2 = node2
1146 1144 try:
1147 1145 if node2 is not None:
1148 1146 tempnode2 = ctx2.substate[subpath][1]
1149 1147 except KeyError:
1150 1148 # A subrepo that existed in node1 was deleted between node1 and
1151 1149 # node2 (inclusive). Thus, ctx2's substate won't contain that
1152 1150 # subpath. The best we can do is to ignore it.
1153 1151 tempnode2 = None
1154 1152 submatch = matchmod.subdirmatcher(subpath, match)
1155 1153 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1156 1154 stat=stat, fp=fp, prefix=prefix)
1157 1155
1158 1156 def _changesetlabels(ctx):
1159 1157 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1160 1158 if ctx.troubled():
1161 1159 labels.append('changeset.troubled')
1162 1160 for trouble in ctx.troubles():
1163 1161 labels.append('trouble.%s' % trouble)
1164 1162 return ' '.join(labels)
1165 1163
1166 1164 class changeset_printer(object):
1167 1165 '''show changeset information when templating not requested.'''
1168 1166
1169 1167 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1170 1168 self.ui = ui
1171 1169 self.repo = repo
1172 1170 self.buffered = buffered
1173 1171 self.matchfn = matchfn
1174 1172 self.diffopts = diffopts
1175 1173 self.header = {}
1176 1174 self.hunk = {}
1177 1175 self.lastheader = None
1178 1176 self.footer = None
1179 1177
1180 1178 def flush(self, ctx):
1181 1179 rev = ctx.rev()
1182 1180 if rev in self.header:
1183 1181 h = self.header[rev]
1184 1182 if h != self.lastheader:
1185 1183 self.lastheader = h
1186 1184 self.ui.write(h)
1187 1185 del self.header[rev]
1188 1186 if rev in self.hunk:
1189 1187 self.ui.write(self.hunk[rev])
1190 1188 del self.hunk[rev]
1191 1189 return 1
1192 1190 return 0
1193 1191
1194 1192 def close(self):
1195 1193 if self.footer:
1196 1194 self.ui.write(self.footer)
1197 1195
1198 1196 def show(self, ctx, copies=None, matchfn=None, **props):
1199 1197 if self.buffered:
1200 1198 self.ui.pushbuffer(labeled=True)
1201 1199 self._show(ctx, copies, matchfn, props)
1202 1200 self.hunk[ctx.rev()] = self.ui.popbuffer()
1203 1201 else:
1204 1202 self._show(ctx, copies, matchfn, props)
1205 1203
1206 1204 def _show(self, ctx, copies, matchfn, props):
1207 1205 '''show a single changeset or file revision'''
1208 1206 changenode = ctx.node()
1209 1207 rev = ctx.rev()
1210 1208 if self.ui.debugflag:
1211 1209 hexfunc = hex
1212 1210 else:
1213 1211 hexfunc = short
1214 1212 # as of now, wctx.node() and wctx.rev() return None, but we want to
1215 1213 # show the same values as {node} and {rev} templatekw
1216 1214 revnode = (scmutil.intrev(rev), hexfunc(bin(ctx.hex())))
1217 1215
1218 1216 if self.ui.quiet:
1219 1217 self.ui.write("%d:%s\n" % revnode, label='log.node')
1220 1218 return
1221 1219
1222 1220 date = util.datestr(ctx.date())
1223 1221
1224 1222 # i18n: column positioning for "hg log"
1225 1223 self.ui.write(_("changeset: %d:%s\n") % revnode,
1226 1224 label=_changesetlabels(ctx))
1227 1225
1228 1226 # branches are shown first before any other names due to backwards
1229 1227 # compatibility
1230 1228 branch = ctx.branch()
1231 1229 # don't show the default branch name
1232 1230 if branch != 'default':
1233 1231 # i18n: column positioning for "hg log"
1234 1232 self.ui.write(_("branch: %s\n") % branch,
1235 1233 label='log.branch')
1236 1234
1237 1235 for nsname, ns in self.repo.names.iteritems():
1238 1236 # branches has special logic already handled above, so here we just
1239 1237 # skip it
1240 1238 if nsname == 'branches':
1241 1239 continue
1242 1240 # we will use the templatename as the color name since those two
1243 1241 # should be the same
1244 1242 for name in ns.names(self.repo, changenode):
1245 1243 self.ui.write(ns.logfmt % name,
1246 1244 label='log.%s' % ns.colorname)
1247 1245 if self.ui.debugflag:
1248 1246 # i18n: column positioning for "hg log"
1249 1247 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1250 1248 label='log.phase')
1251 1249 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1252 1250 label = 'log.parent changeset.%s' % pctx.phasestr()
1253 1251 # i18n: column positioning for "hg log"
1254 1252 self.ui.write(_("parent: %d:%s\n")
1255 1253 % (pctx.rev(), hexfunc(pctx.node())),
1256 1254 label=label)
1257 1255
1258 1256 if self.ui.debugflag and rev is not None:
1259 1257 mnode = ctx.manifestnode()
1260 1258 # i18n: column positioning for "hg log"
1261 1259 self.ui.write(_("manifest: %d:%s\n") %
1262 1260 (self.repo.manifestlog._revlog.rev(mnode),
1263 1261 hex(mnode)),
1264 1262 label='ui.debug log.manifest')
1265 1263 # i18n: column positioning for "hg log"
1266 1264 self.ui.write(_("user: %s\n") % ctx.user(),
1267 1265 label='log.user')
1268 1266 # i18n: column positioning for "hg log"
1269 1267 self.ui.write(_("date: %s\n") % date,
1270 1268 label='log.date')
1271 1269
1272 1270 if ctx.troubled():
1273 1271 # i18n: column positioning for "hg log"
1274 1272 self.ui.write(_("trouble: %s\n") % ', '.join(ctx.troubles()),
1275 1273 label='log.trouble')
1276 1274
1277 1275 if self.ui.debugflag:
1278 1276 files = ctx.p1().status(ctx)[:3]
1279 1277 for key, value in zip([# i18n: column positioning for "hg log"
1280 1278 _("files:"),
1281 1279 # i18n: column positioning for "hg log"
1282 1280 _("files+:"),
1283 1281 # i18n: column positioning for "hg log"
1284 1282 _("files-:")], files):
1285 1283 if value:
1286 1284 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1287 1285 label='ui.debug log.files')
1288 1286 elif ctx.files() and self.ui.verbose:
1289 1287 # i18n: column positioning for "hg log"
1290 1288 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1291 1289 label='ui.note log.files')
1292 1290 if copies and self.ui.verbose:
1293 1291 copies = ['%s (%s)' % c for c in copies]
1294 1292 # i18n: column positioning for "hg log"
1295 1293 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1296 1294 label='ui.note log.copies')
1297 1295
1298 1296 extra = ctx.extra()
1299 1297 if extra and self.ui.debugflag:
1300 1298 for key, value in sorted(extra.items()):
1301 1299 # i18n: column positioning for "hg log"
1302 1300 self.ui.write(_("extra: %s=%s\n")
1303 1301 % (key, util.escapestr(value)),
1304 1302 label='ui.debug log.extra')
1305 1303
1306 1304 description = ctx.description().strip()
1307 1305 if description:
1308 1306 if self.ui.verbose:
1309 1307 self.ui.write(_("description:\n"),
1310 1308 label='ui.note log.description')
1311 1309 self.ui.write(description,
1312 1310 label='ui.note log.description')
1313 1311 self.ui.write("\n\n")
1314 1312 else:
1315 1313 # i18n: column positioning for "hg log"
1316 1314 self.ui.write(_("summary: %s\n") %
1317 1315 description.splitlines()[0],
1318 1316 label='log.summary')
1319 1317 self.ui.write("\n")
1320 1318
1321 1319 self.showpatch(ctx, matchfn)
1322 1320
1323 1321 def showpatch(self, ctx, matchfn):
1324 1322 if not matchfn:
1325 1323 matchfn = self.matchfn
1326 1324 if matchfn:
1327 1325 stat = self.diffopts.get('stat')
1328 1326 diff = self.diffopts.get('patch')
1329 1327 diffopts = patch.diffallopts(self.ui, self.diffopts)
1330 1328 node = ctx.node()
1331 1329 prev = ctx.p1().node()
1332 1330 if stat:
1333 1331 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1334 1332 match=matchfn, stat=True)
1335 1333 if diff:
1336 1334 if stat:
1337 1335 self.ui.write("\n")
1338 1336 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1339 1337 match=matchfn, stat=False)
1340 1338 self.ui.write("\n")
1341 1339
1342 1340 class jsonchangeset(changeset_printer):
1343 1341 '''format changeset information.'''
1344 1342
1345 1343 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1346 1344 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1347 1345 self.cache = {}
1348 1346 self._first = True
1349 1347
1350 1348 def close(self):
1351 1349 if not self._first:
1352 1350 self.ui.write("\n]\n")
1353 1351 else:
1354 1352 self.ui.write("[]\n")
1355 1353
1356 1354 def _show(self, ctx, copies, matchfn, props):
1357 1355 '''show a single changeset or file revision'''
1358 1356 rev = ctx.rev()
1359 1357 if rev is None:
1360 1358 jrev = jnode = 'null'
1361 1359 else:
1362 1360 jrev = str(rev)
1363 1361 jnode = '"%s"' % hex(ctx.node())
1364 1362 j = encoding.jsonescape
1365 1363
1366 1364 if self._first:
1367 1365 self.ui.write("[\n {")
1368 1366 self._first = False
1369 1367 else:
1370 1368 self.ui.write(",\n {")
1371 1369
1372 1370 if self.ui.quiet:
1373 1371 self.ui.write(('\n "rev": %s') % jrev)
1374 1372 self.ui.write((',\n "node": %s') % jnode)
1375 1373 self.ui.write('\n }')
1376 1374 return
1377 1375
1378 1376 self.ui.write(('\n "rev": %s') % jrev)
1379 1377 self.ui.write((',\n "node": %s') % jnode)
1380 1378 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1381 1379 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1382 1380 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1383 1381 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1384 1382 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1385 1383
1386 1384 self.ui.write((',\n "bookmarks": [%s]') %
1387 1385 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1388 1386 self.ui.write((',\n "tags": [%s]') %
1389 1387 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1390 1388 self.ui.write((',\n "parents": [%s]') %
1391 1389 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1392 1390
1393 1391 if self.ui.debugflag:
1394 1392 if rev is None:
1395 1393 jmanifestnode = 'null'
1396 1394 else:
1397 1395 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1398 1396 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1399 1397
1400 1398 self.ui.write((',\n "extra": {%s}') %
1401 1399 ", ".join('"%s": "%s"' % (j(k), j(v))
1402 1400 for k, v in ctx.extra().items()))
1403 1401
1404 1402 files = ctx.p1().status(ctx)
1405 1403 self.ui.write((',\n "modified": [%s]') %
1406 1404 ", ".join('"%s"' % j(f) for f in files[0]))
1407 1405 self.ui.write((',\n "added": [%s]') %
1408 1406 ", ".join('"%s"' % j(f) for f in files[1]))
1409 1407 self.ui.write((',\n "removed": [%s]') %
1410 1408 ", ".join('"%s"' % j(f) for f in files[2]))
1411 1409
1412 1410 elif self.ui.verbose:
1413 1411 self.ui.write((',\n "files": [%s]') %
1414 1412 ", ".join('"%s"' % j(f) for f in ctx.files()))
1415 1413
1416 1414 if copies:
1417 1415 self.ui.write((',\n "copies": {%s}') %
1418 1416 ", ".join('"%s": "%s"' % (j(k), j(v))
1419 1417 for k, v in copies))
1420 1418
1421 1419 matchfn = self.matchfn
1422 1420 if matchfn:
1423 1421 stat = self.diffopts.get('stat')
1424 1422 diff = self.diffopts.get('patch')
1425 1423 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1426 1424 node, prev = ctx.node(), ctx.p1().node()
1427 1425 if stat:
1428 1426 self.ui.pushbuffer()
1429 1427 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1430 1428 match=matchfn, stat=True)
1431 1429 self.ui.write((',\n "diffstat": "%s"')
1432 1430 % j(self.ui.popbuffer()))
1433 1431 if diff:
1434 1432 self.ui.pushbuffer()
1435 1433 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1436 1434 match=matchfn, stat=False)
1437 1435 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1438 1436
1439 1437 self.ui.write("\n }")
1440 1438
1441 1439 class changeset_templater(changeset_printer):
1442 1440 '''format changeset information.'''
1443 1441
1444 1442 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1445 1443 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1446 1444 assert not (tmpl and mapfile)
1447 1445 defaulttempl = templatekw.defaulttempl
1448 1446 if mapfile:
1449 1447 self.t = templater.templater.frommapfile(mapfile,
1450 1448 cache=defaulttempl)
1451 1449 else:
1452 1450 self.t = formatter.maketemplater(ui, 'changeset', tmpl,
1453 1451 cache=defaulttempl)
1454 1452
1455 1453 self.cache = {}
1456 1454
1457 1455 # find correct templates for current mode
1458 1456 tmplmodes = [
1459 1457 (True, None),
1460 1458 (self.ui.verbose, 'verbose'),
1461 1459 (self.ui.quiet, 'quiet'),
1462 1460 (self.ui.debugflag, 'debug'),
1463 1461 ]
1464 1462
1465 1463 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1466 1464 'docheader': '', 'docfooter': ''}
1467 1465 for mode, postfix in tmplmodes:
1468 1466 for t in self._parts:
1469 1467 cur = t
1470 1468 if postfix:
1471 1469 cur += "_" + postfix
1472 1470 if mode and cur in self.t:
1473 1471 self._parts[t] = cur
1474 1472
1475 1473 if self._parts['docheader']:
1476 1474 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1477 1475
1478 1476 def close(self):
1479 1477 if self._parts['docfooter']:
1480 1478 if not self.footer:
1481 1479 self.footer = ""
1482 1480 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1483 1481 return super(changeset_templater, self).close()
1484 1482
1485 1483 def _show(self, ctx, copies, matchfn, props):
1486 1484 '''show a single changeset or file revision'''
1487 1485 props = props.copy()
1488 1486 props.update(templatekw.keywords)
1489 1487 props['templ'] = self.t
1490 1488 props['ctx'] = ctx
1491 1489 props['repo'] = self.repo
1492 1490 props['ui'] = self.repo.ui
1493 1491 props['revcache'] = {'copies': copies}
1494 1492 props['cache'] = self.cache
1495 1493
1496 1494 # write header
1497 1495 if self._parts['header']:
1498 1496 h = templater.stringify(self.t(self._parts['header'], **props))
1499 1497 if self.buffered:
1500 1498 self.header[ctx.rev()] = h
1501 1499 else:
1502 1500 if self.lastheader != h:
1503 1501 self.lastheader = h
1504 1502 self.ui.write(h)
1505 1503
1506 1504 # write changeset metadata, then patch if requested
1507 1505 key = self._parts['changeset']
1508 1506 self.ui.write(templater.stringify(self.t(key, **props)))
1509 1507 self.showpatch(ctx, matchfn)
1510 1508
1511 1509 if self._parts['footer']:
1512 1510 if not self.footer:
1513 1511 self.footer = templater.stringify(
1514 1512 self.t(self._parts['footer'], **props))
1515 1513
1516 1514 def gettemplate(ui, tmpl, style):
1517 1515 """
1518 1516 Find the template matching the given template spec or style.
1519 1517 """
1520 1518
1521 1519 # ui settings
1522 1520 if not tmpl and not style: # template are stronger than style
1523 1521 tmpl = ui.config('ui', 'logtemplate')
1524 1522 if tmpl:
1525 1523 return templater.unquotestring(tmpl), None
1526 1524 else:
1527 1525 style = util.expandpath(ui.config('ui', 'style', ''))
1528 1526
1529 1527 if not tmpl and style:
1530 1528 mapfile = style
1531 1529 if not os.path.split(mapfile)[0]:
1532 1530 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1533 1531 or templater.templatepath(mapfile))
1534 1532 if mapname:
1535 1533 mapfile = mapname
1536 1534 return None, mapfile
1537 1535
1538 1536 if not tmpl:
1539 1537 return None, None
1540 1538
1541 1539 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1542 1540
1543 1541 def show_changeset(ui, repo, opts, buffered=False):
1544 1542 """show one changeset using template or regular display.
1545 1543
1546 1544 Display format will be the first non-empty hit of:
1547 1545 1. option 'template'
1548 1546 2. option 'style'
1549 1547 3. [ui] setting 'logtemplate'
1550 1548 4. [ui] setting 'style'
1551 1549 If all of these values are either the unset or the empty string,
1552 1550 regular display via changeset_printer() is done.
1553 1551 """
1554 1552 # options
1555 1553 matchfn = None
1556 1554 if opts.get('patch') or opts.get('stat'):
1557 1555 matchfn = scmutil.matchall(repo)
1558 1556
1559 1557 if opts.get('template') == 'json':
1560 1558 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1561 1559
1562 1560 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1563 1561
1564 1562 if not tmpl and not mapfile:
1565 1563 return changeset_printer(ui, repo, matchfn, opts, buffered)
1566 1564
1567 1565 return changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile, buffered)
1568 1566
1569 1567 def showmarker(fm, marker, index=None):
1570 1568 """utility function to display obsolescence marker in a readable way
1571 1569
1572 1570 To be used by debug function."""
1573 1571 if index is not None:
1574 1572 fm.write('index', '%i ', index)
1575 1573 fm.write('precnode', '%s ', hex(marker.precnode()))
1576 1574 succs = marker.succnodes()
1577 1575 fm.condwrite(succs, 'succnodes', '%s ',
1578 1576 fm.formatlist(map(hex, succs), name='node'))
1579 1577 fm.write('flag', '%X ', marker.flags())
1580 1578 parents = marker.parentnodes()
1581 1579 if parents is not None:
1582 1580 fm.write('parentnodes', '{%s} ',
1583 1581 fm.formatlist(map(hex, parents), name='node', sep=', '))
1584 1582 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1585 1583 meta = marker.metadata().copy()
1586 1584 meta.pop('date', None)
1587 1585 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
1588 1586 fm.plain('\n')
1589 1587
1590 1588 def finddate(ui, repo, date):
1591 1589 """Find the tipmost changeset that matches the given date spec"""
1592 1590
1593 1591 df = util.matchdate(date)
1594 1592 m = scmutil.matchall(repo)
1595 1593 results = {}
1596 1594
1597 1595 def prep(ctx, fns):
1598 1596 d = ctx.date()
1599 1597 if df(d[0]):
1600 1598 results[ctx.rev()] = d
1601 1599
1602 1600 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1603 1601 rev = ctx.rev()
1604 1602 if rev in results:
1605 1603 ui.status(_("found revision %s from %s\n") %
1606 1604 (rev, util.datestr(results[rev])))
1607 1605 return str(rev)
1608 1606
1609 1607 raise error.Abort(_("revision matching date not found"))
1610 1608
1611 1609 def increasingwindows(windowsize=8, sizelimit=512):
1612 1610 while True:
1613 1611 yield windowsize
1614 1612 if windowsize < sizelimit:
1615 1613 windowsize *= 2
1616 1614
1617 1615 class FileWalkError(Exception):
1618 1616 pass
1619 1617
1620 1618 def walkfilerevs(repo, match, follow, revs, fncache):
1621 1619 '''Walks the file history for the matched files.
1622 1620
1623 1621 Returns the changeset revs that are involved in the file history.
1624 1622
1625 1623 Throws FileWalkError if the file history can't be walked using
1626 1624 filelogs alone.
1627 1625 '''
1628 1626 wanted = set()
1629 1627 copies = []
1630 1628 minrev, maxrev = min(revs), max(revs)
1631 1629 def filerevgen(filelog, last):
1632 1630 """
1633 1631 Only files, no patterns. Check the history of each file.
1634 1632
1635 1633 Examines filelog entries within minrev, maxrev linkrev range
1636 1634 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1637 1635 tuples in backwards order
1638 1636 """
1639 1637 cl_count = len(repo)
1640 1638 revs = []
1641 1639 for j in xrange(0, last + 1):
1642 1640 linkrev = filelog.linkrev(j)
1643 1641 if linkrev < minrev:
1644 1642 continue
1645 1643 # only yield rev for which we have the changelog, it can
1646 1644 # happen while doing "hg log" during a pull or commit
1647 1645 if linkrev >= cl_count:
1648 1646 break
1649 1647
1650 1648 parentlinkrevs = []
1651 1649 for p in filelog.parentrevs(j):
1652 1650 if p != nullrev:
1653 1651 parentlinkrevs.append(filelog.linkrev(p))
1654 1652 n = filelog.node(j)
1655 1653 revs.append((linkrev, parentlinkrevs,
1656 1654 follow and filelog.renamed(n)))
1657 1655
1658 1656 return reversed(revs)
1659 1657 def iterfiles():
1660 1658 pctx = repo['.']
1661 1659 for filename in match.files():
1662 1660 if follow:
1663 1661 if filename not in pctx:
1664 1662 raise error.Abort(_('cannot follow file not in parent '
1665 1663 'revision: "%s"') % filename)
1666 1664 yield filename, pctx[filename].filenode()
1667 1665 else:
1668 1666 yield filename, None
1669 1667 for filename_node in copies:
1670 1668 yield filename_node
1671 1669
1672 1670 for file_, node in iterfiles():
1673 1671 filelog = repo.file(file_)
1674 1672 if not len(filelog):
1675 1673 if node is None:
1676 1674 # A zero count may be a directory or deleted file, so
1677 1675 # try to find matching entries on the slow path.
1678 1676 if follow:
1679 1677 raise error.Abort(
1680 1678 _('cannot follow nonexistent file: "%s"') % file_)
1681 1679 raise FileWalkError("Cannot walk via filelog")
1682 1680 else:
1683 1681 continue
1684 1682
1685 1683 if node is None:
1686 1684 last = len(filelog) - 1
1687 1685 else:
1688 1686 last = filelog.rev(node)
1689 1687
1690 1688 # keep track of all ancestors of the file
1691 1689 ancestors = set([filelog.linkrev(last)])
1692 1690
1693 1691 # iterate from latest to oldest revision
1694 1692 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1695 1693 if not follow:
1696 1694 if rev > maxrev:
1697 1695 continue
1698 1696 else:
1699 1697 # Note that last might not be the first interesting
1700 1698 # rev to us:
1701 1699 # if the file has been changed after maxrev, we'll
1702 1700 # have linkrev(last) > maxrev, and we still need
1703 1701 # to explore the file graph
1704 1702 if rev not in ancestors:
1705 1703 continue
1706 1704 # XXX insert 1327 fix here
1707 1705 if flparentlinkrevs:
1708 1706 ancestors.update(flparentlinkrevs)
1709 1707
1710 1708 fncache.setdefault(rev, []).append(file_)
1711 1709 wanted.add(rev)
1712 1710 if copied:
1713 1711 copies.append(copied)
1714 1712
1715 1713 return wanted
1716 1714
1717 1715 class _followfilter(object):
1718 1716 def __init__(self, repo, onlyfirst=False):
1719 1717 self.repo = repo
1720 1718 self.startrev = nullrev
1721 1719 self.roots = set()
1722 1720 self.onlyfirst = onlyfirst
1723 1721
1724 1722 def match(self, rev):
1725 1723 def realparents(rev):
1726 1724 if self.onlyfirst:
1727 1725 return self.repo.changelog.parentrevs(rev)[0:1]
1728 1726 else:
1729 1727 return filter(lambda x: x != nullrev,
1730 1728 self.repo.changelog.parentrevs(rev))
1731 1729
1732 1730 if self.startrev == nullrev:
1733 1731 self.startrev = rev
1734 1732 return True
1735 1733
1736 1734 if rev > self.startrev:
1737 1735 # forward: all descendants
1738 1736 if not self.roots:
1739 1737 self.roots.add(self.startrev)
1740 1738 for parent in realparents(rev):
1741 1739 if parent in self.roots:
1742 1740 self.roots.add(rev)
1743 1741 return True
1744 1742 else:
1745 1743 # backwards: all parents
1746 1744 if not self.roots:
1747 1745 self.roots.update(realparents(self.startrev))
1748 1746 if rev in self.roots:
1749 1747 self.roots.remove(rev)
1750 1748 self.roots.update(realparents(rev))
1751 1749 return True
1752 1750
1753 1751 return False
1754 1752
1755 1753 def walkchangerevs(repo, match, opts, prepare):
1756 1754 '''Iterate over files and the revs in which they changed.
1757 1755
1758 1756 Callers most commonly need to iterate backwards over the history
1759 1757 in which they are interested. Doing so has awful (quadratic-looking)
1760 1758 performance, so we use iterators in a "windowed" way.
1761 1759
1762 1760 We walk a window of revisions in the desired order. Within the
1763 1761 window, we first walk forwards to gather data, then in the desired
1764 1762 order (usually backwards) to display it.
1765 1763
1766 1764 This function returns an iterator yielding contexts. Before
1767 1765 yielding each context, the iterator will first call the prepare
1768 1766 function on each context in the window in forward order.'''
1769 1767
1770 1768 follow = opts.get('follow') or opts.get('follow_first')
1771 1769 revs = _logrevs(repo, opts)
1772 1770 if not revs:
1773 1771 return []
1774 1772 wanted = set()
1775 1773 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1776 1774 opts.get('removed'))
1777 1775 fncache = {}
1778 1776 change = repo.changectx
1779 1777
1780 1778 # First step is to fill wanted, the set of revisions that we want to yield.
1781 1779 # When it does not induce extra cost, we also fill fncache for revisions in
1782 1780 # wanted: a cache of filenames that were changed (ctx.files()) and that
1783 1781 # match the file filtering conditions.
1784 1782
1785 1783 if match.always():
1786 1784 # No files, no patterns. Display all revs.
1787 1785 wanted = revs
1788 1786 elif not slowpath:
1789 1787 # We only have to read through the filelog to find wanted revisions
1790 1788
1791 1789 try:
1792 1790 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1793 1791 except FileWalkError:
1794 1792 slowpath = True
1795 1793
1796 1794 # We decided to fall back to the slowpath because at least one
1797 1795 # of the paths was not a file. Check to see if at least one of them
1798 1796 # existed in history, otherwise simply return
1799 1797 for path in match.files():
1800 1798 if path == '.' or path in repo.store:
1801 1799 break
1802 1800 else:
1803 1801 return []
1804 1802
1805 1803 if slowpath:
1806 1804 # We have to read the changelog to match filenames against
1807 1805 # changed files
1808 1806
1809 1807 if follow:
1810 1808 raise error.Abort(_('can only follow copies/renames for explicit '
1811 1809 'filenames'))
1812 1810
1813 1811 # The slow path checks files modified in every changeset.
1814 1812 # This is really slow on large repos, so compute the set lazily.
1815 1813 class lazywantedset(object):
1816 1814 def __init__(self):
1817 1815 self.set = set()
1818 1816 self.revs = set(revs)
1819 1817
1820 1818 # No need to worry about locality here because it will be accessed
1821 1819 # in the same order as the increasing window below.
1822 1820 def __contains__(self, value):
1823 1821 if value in self.set:
1824 1822 return True
1825 1823 elif not value in self.revs:
1826 1824 return False
1827 1825 else:
1828 1826 self.revs.discard(value)
1829 1827 ctx = change(value)
1830 1828 matches = filter(match, ctx.files())
1831 1829 if matches:
1832 1830 fncache[value] = matches
1833 1831 self.set.add(value)
1834 1832 return True
1835 1833 return False
1836 1834
1837 1835 def discard(self, value):
1838 1836 self.revs.discard(value)
1839 1837 self.set.discard(value)
1840 1838
1841 1839 wanted = lazywantedset()
1842 1840
1843 1841 # it might be worthwhile to do this in the iterator if the rev range
1844 1842 # is descending and the prune args are all within that range
1845 1843 for rev in opts.get('prune', ()):
1846 1844 rev = repo[rev].rev()
1847 1845 ff = _followfilter(repo)
1848 1846 stop = min(revs[0], revs[-1])
1849 1847 for x in xrange(rev, stop - 1, -1):
1850 1848 if ff.match(x):
1851 1849 wanted = wanted - [x]
1852 1850
1853 1851 # Now that wanted is correctly initialized, we can iterate over the
1854 1852 # revision range, yielding only revisions in wanted.
1855 1853 def iterate():
1856 1854 if follow and match.always():
1857 1855 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1858 1856 def want(rev):
1859 1857 return ff.match(rev) and rev in wanted
1860 1858 else:
1861 1859 def want(rev):
1862 1860 return rev in wanted
1863 1861
1864 1862 it = iter(revs)
1865 1863 stopiteration = False
1866 1864 for windowsize in increasingwindows():
1867 1865 nrevs = []
1868 1866 for i in xrange(windowsize):
1869 1867 rev = next(it, None)
1870 1868 if rev is None:
1871 1869 stopiteration = True
1872 1870 break
1873 1871 elif want(rev):
1874 1872 nrevs.append(rev)
1875 1873 for rev in sorted(nrevs):
1876 1874 fns = fncache.get(rev)
1877 1875 ctx = change(rev)
1878 1876 if not fns:
1879 1877 def fns_generator():
1880 1878 for f in ctx.files():
1881 1879 if match(f):
1882 1880 yield f
1883 1881 fns = fns_generator()
1884 1882 prepare(ctx, fns)
1885 1883 for rev in nrevs:
1886 1884 yield change(rev)
1887 1885
1888 1886 if stopiteration:
1889 1887 break
1890 1888
1891 1889 return iterate()
1892 1890
1893 1891 def _makefollowlogfilematcher(repo, files, followfirst):
1894 1892 # When displaying a revision with --patch --follow FILE, we have
1895 1893 # to know which file of the revision must be diffed. With
1896 1894 # --follow, we want the names of the ancestors of FILE in the
1897 1895 # revision, stored in "fcache". "fcache" is populated by
1898 1896 # reproducing the graph traversal already done by --follow revset
1899 1897 # and relating revs to file names (which is not "correct" but
1900 1898 # good enough).
1901 1899 fcache = {}
1902 1900 fcacheready = [False]
1903 1901 pctx = repo['.']
1904 1902
1905 1903 def populate():
1906 1904 for fn in files:
1907 1905 fctx = pctx[fn]
1908 1906 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
1909 1907 for c in fctx.ancestors(followfirst=followfirst):
1910 1908 fcache.setdefault(c.rev(), set()).add(c.path())
1911 1909
1912 1910 def filematcher(rev):
1913 1911 if not fcacheready[0]:
1914 1912 # Lazy initialization
1915 1913 fcacheready[0] = True
1916 1914 populate()
1917 1915 return scmutil.matchfiles(repo, fcache.get(rev, []))
1918 1916
1919 1917 return filematcher
1920 1918
1921 1919 def _makenofollowlogfilematcher(repo, pats, opts):
1922 1920 '''hook for extensions to override the filematcher for non-follow cases'''
1923 1921 return None
1924 1922
1925 1923 def _makelogrevset(repo, pats, opts, revs):
1926 1924 """Return (expr, filematcher) where expr is a revset string built
1927 1925 from log options and file patterns or None. If --stat or --patch
1928 1926 are not passed filematcher is None. Otherwise it is a callable
1929 1927 taking a revision number and returning a match objects filtering
1930 1928 the files to be detailed when displaying the revision.
1931 1929 """
1932 1930 opt2revset = {
1933 1931 'no_merges': ('not merge()', None),
1934 1932 'only_merges': ('merge()', None),
1935 1933 '_ancestors': ('ancestors(%(val)s)', None),
1936 1934 '_fancestors': ('_firstancestors(%(val)s)', None),
1937 1935 '_descendants': ('descendants(%(val)s)', None),
1938 1936 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1939 1937 '_matchfiles': ('_matchfiles(%(val)s)', None),
1940 1938 'date': ('date(%(val)r)', None),
1941 1939 'branch': ('branch(%(val)r)', ' or '),
1942 1940 '_patslog': ('filelog(%(val)r)', ' or '),
1943 1941 '_patsfollow': ('follow(%(val)r)', ' or '),
1944 1942 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1945 1943 'keyword': ('keyword(%(val)r)', ' or '),
1946 1944 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1947 1945 'user': ('user(%(val)r)', ' or '),
1948 1946 }
1949 1947
1950 1948 opts = dict(opts)
1951 1949 # follow or not follow?
1952 1950 follow = opts.get('follow') or opts.get('follow_first')
1953 1951 if opts.get('follow_first'):
1954 1952 followfirst = 1
1955 1953 else:
1956 1954 followfirst = 0
1957 1955 # --follow with FILE behavior depends on revs...
1958 1956 it = iter(revs)
1959 1957 startrev = next(it)
1960 1958 followdescendants = startrev < next(it, startrev)
1961 1959
1962 1960 # branch and only_branch are really aliases and must be handled at
1963 1961 # the same time
1964 1962 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1965 1963 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1966 1964 # pats/include/exclude are passed to match.match() directly in
1967 1965 # _matchfiles() revset but walkchangerevs() builds its matcher with
1968 1966 # scmutil.match(). The difference is input pats are globbed on
1969 1967 # platforms without shell expansion (windows).
1970 1968 wctx = repo[None]
1971 1969 match, pats = scmutil.matchandpats(wctx, pats, opts)
1972 1970 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1973 1971 opts.get('removed'))
1974 1972 if not slowpath:
1975 1973 for f in match.files():
1976 1974 if follow and f not in wctx:
1977 1975 # If the file exists, it may be a directory, so let it
1978 1976 # take the slow path.
1979 1977 if os.path.exists(repo.wjoin(f)):
1980 1978 slowpath = True
1981 1979 continue
1982 1980 else:
1983 1981 raise error.Abort(_('cannot follow file not in parent '
1984 1982 'revision: "%s"') % f)
1985 1983 filelog = repo.file(f)
1986 1984 if not filelog:
1987 1985 # A zero count may be a directory or deleted file, so
1988 1986 # try to find matching entries on the slow path.
1989 1987 if follow:
1990 1988 raise error.Abort(
1991 1989 _('cannot follow nonexistent file: "%s"') % f)
1992 1990 slowpath = True
1993 1991
1994 1992 # We decided to fall back to the slowpath because at least one
1995 1993 # of the paths was not a file. Check to see if at least one of them
1996 1994 # existed in history - in that case, we'll continue down the
1997 1995 # slowpath; otherwise, we can turn off the slowpath
1998 1996 if slowpath:
1999 1997 for path in match.files():
2000 1998 if path == '.' or path in repo.store:
2001 1999 break
2002 2000 else:
2003 2001 slowpath = False
2004 2002
2005 2003 fpats = ('_patsfollow', '_patsfollowfirst')
2006 2004 fnopats = (('_ancestors', '_fancestors'),
2007 2005 ('_descendants', '_fdescendants'))
2008 2006 if slowpath:
2009 2007 # See walkchangerevs() slow path.
2010 2008 #
2011 2009 # pats/include/exclude cannot be represented as separate
2012 2010 # revset expressions as their filtering logic applies at file
2013 2011 # level. For instance "-I a -X a" matches a revision touching
2014 2012 # "a" and "b" while "file(a) and not file(b)" does
2015 2013 # not. Besides, filesets are evaluated against the working
2016 2014 # directory.
2017 2015 matchargs = ['r:', 'd:relpath']
2018 2016 for p in pats:
2019 2017 matchargs.append('p:' + p)
2020 2018 for p in opts.get('include', []):
2021 2019 matchargs.append('i:' + p)
2022 2020 for p in opts.get('exclude', []):
2023 2021 matchargs.append('x:' + p)
2024 2022 matchargs = ','.join(('%r' % p) for p in matchargs)
2025 2023 opts['_matchfiles'] = matchargs
2026 2024 if follow:
2027 2025 opts[fnopats[0][followfirst]] = '.'
2028 2026 else:
2029 2027 if follow:
2030 2028 if pats:
2031 2029 # follow() revset interprets its file argument as a
2032 2030 # manifest entry, so use match.files(), not pats.
2033 2031 opts[fpats[followfirst]] = list(match.files())
2034 2032 else:
2035 2033 op = fnopats[followdescendants][followfirst]
2036 2034 opts[op] = 'rev(%d)' % startrev
2037 2035 else:
2038 2036 opts['_patslog'] = list(pats)
2039 2037
2040 2038 filematcher = None
2041 2039 if opts.get('patch') or opts.get('stat'):
2042 2040 # When following files, track renames via a special matcher.
2043 2041 # If we're forced to take the slowpath it means we're following
2044 2042 # at least one pattern/directory, so don't bother with rename tracking.
2045 2043 if follow and not match.always() and not slowpath:
2046 2044 # _makefollowlogfilematcher expects its files argument to be
2047 2045 # relative to the repo root, so use match.files(), not pats.
2048 2046 filematcher = _makefollowlogfilematcher(repo, match.files(),
2049 2047 followfirst)
2050 2048 else:
2051 2049 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2052 2050 if filematcher is None:
2053 2051 filematcher = lambda rev: match
2054 2052
2055 2053 expr = []
2056 2054 for op, val in sorted(opts.iteritems()):
2057 2055 if not val:
2058 2056 continue
2059 2057 if op not in opt2revset:
2060 2058 continue
2061 2059 revop, andor = opt2revset[op]
2062 2060 if '%(val)' not in revop:
2063 2061 expr.append(revop)
2064 2062 else:
2065 2063 if not isinstance(val, list):
2066 2064 e = revop % {'val': val}
2067 2065 else:
2068 2066 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2069 2067 expr.append(e)
2070 2068
2071 2069 if expr:
2072 2070 expr = '(' + ' and '.join(expr) + ')'
2073 2071 else:
2074 2072 expr = None
2075 2073 return expr, filematcher
2076 2074
2077 2075 def _logrevs(repo, opts):
2078 2076 # Default --rev value depends on --follow but --follow behavior
2079 2077 # depends on revisions resolved from --rev...
2080 2078 follow = opts.get('follow') or opts.get('follow_first')
2081 2079 if opts.get('rev'):
2082 2080 revs = scmutil.revrange(repo, opts['rev'])
2083 2081 elif follow and repo.dirstate.p1() == nullid:
2084 2082 revs = smartset.baseset()
2085 2083 elif follow:
2086 2084 revs = repo.revs('reverse(:.)')
2087 2085 else:
2088 2086 revs = smartset.spanset(repo)
2089 2087 revs.reverse()
2090 2088 return revs
2091 2089
2092 2090 def getgraphlogrevs(repo, pats, opts):
2093 2091 """Return (revs, expr, filematcher) where revs is an iterable of
2094 2092 revision numbers, expr is a revset string built from log options
2095 2093 and file patterns or None, and used to filter 'revs'. If --stat or
2096 2094 --patch are not passed filematcher is None. Otherwise it is a
2097 2095 callable taking a revision number and returning a match objects
2098 2096 filtering the files to be detailed when displaying the revision.
2099 2097 """
2100 2098 limit = loglimit(opts)
2101 2099 revs = _logrevs(repo, opts)
2102 2100 if not revs:
2103 2101 return smartset.baseset(), None, None
2104 2102 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2105 2103 if opts.get('rev'):
2106 2104 # User-specified revs might be unsorted, but don't sort before
2107 2105 # _makelogrevset because it might depend on the order of revs
2108 2106 if not (revs.isdescending() or revs.istopo()):
2109 2107 revs.sort(reverse=True)
2110 2108 if expr:
2111 2109 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2112 2110 revs = matcher(repo, revs)
2113 2111 if limit is not None:
2114 2112 limitedrevs = []
2115 2113 for idx, rev in enumerate(revs):
2116 2114 if idx >= limit:
2117 2115 break
2118 2116 limitedrevs.append(rev)
2119 2117 revs = smartset.baseset(limitedrevs)
2120 2118
2121 2119 return revs, expr, filematcher
2122 2120
2123 2121 def getlogrevs(repo, pats, opts):
2124 2122 """Return (revs, expr, filematcher) where revs is an iterable of
2125 2123 revision numbers, expr is a revset string built from log options
2126 2124 and file patterns or None, and used to filter 'revs'. If --stat or
2127 2125 --patch are not passed filematcher is None. Otherwise it is a
2128 2126 callable taking a revision number and returning a match objects
2129 2127 filtering the files to be detailed when displaying the revision.
2130 2128 """
2131 2129 limit = loglimit(opts)
2132 2130 revs = _logrevs(repo, opts)
2133 2131 if not revs:
2134 2132 return smartset.baseset([]), None, None
2135 2133 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2136 2134 if expr:
2137 2135 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2138 2136 revs = matcher(repo, revs)
2139 2137 if limit is not None:
2140 2138 limitedrevs = []
2141 2139 for idx, r in enumerate(revs):
2142 2140 if limit <= idx:
2143 2141 break
2144 2142 limitedrevs.append(r)
2145 2143 revs = smartset.baseset(limitedrevs)
2146 2144
2147 2145 return revs, expr, filematcher
2148 2146
2149 2147 def _graphnodeformatter(ui, displayer):
2150 2148 spec = ui.config('ui', 'graphnodetemplate')
2151 2149 if not spec:
2152 2150 return templatekw.showgraphnode # fast path for "{graphnode}"
2153 2151
2154 2152 templ = formatter.gettemplater(ui, 'graphnode', spec)
2155 2153 cache = {}
2156 2154 if isinstance(displayer, changeset_templater):
2157 2155 cache = displayer.cache # reuse cache of slow templates
2158 2156 props = templatekw.keywords.copy()
2159 2157 props['templ'] = templ
2160 2158 props['cache'] = cache
2161 2159 def formatnode(repo, ctx):
2162 2160 props['ctx'] = ctx
2163 2161 props['repo'] = repo
2164 2162 props['ui'] = repo.ui
2165 2163 props['revcache'] = {}
2166 2164 return templater.stringify(templ('graphnode', **props))
2167 2165 return formatnode
2168 2166
2169 2167 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2170 2168 filematcher=None):
2171 2169 formatnode = _graphnodeformatter(ui, displayer)
2172 2170 state = graphmod.asciistate()
2173 2171 styles = state['styles']
2174 2172
2175 2173 # only set graph styling if HGPLAIN is not set.
2176 2174 if ui.plain('graph'):
2177 2175 # set all edge styles to |, the default pre-3.8 behaviour
2178 2176 styles.update(dict.fromkeys(styles, '|'))
2179 2177 else:
2180 2178 edgetypes = {
2181 2179 'parent': graphmod.PARENT,
2182 2180 'grandparent': graphmod.GRANDPARENT,
2183 2181 'missing': graphmod.MISSINGPARENT
2184 2182 }
2185 2183 for name, key in edgetypes.items():
2186 2184 # experimental config: experimental.graphstyle.*
2187 2185 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2188 2186 styles[key])
2189 2187 if not styles[key]:
2190 2188 styles[key] = None
2191 2189
2192 2190 # experimental config: experimental.graphshorten
2193 2191 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2194 2192
2195 2193 for rev, type, ctx, parents in dag:
2196 2194 char = formatnode(repo, ctx)
2197 2195 copies = None
2198 2196 if getrenamed and ctx.rev():
2199 2197 copies = []
2200 2198 for fn in ctx.files():
2201 2199 rename = getrenamed(fn, ctx.rev())
2202 2200 if rename:
2203 2201 copies.append((fn, rename[0]))
2204 2202 revmatchfn = None
2205 2203 if filematcher is not None:
2206 2204 revmatchfn = filematcher(ctx.rev())
2207 2205 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2208 2206 lines = displayer.hunk.pop(rev).split('\n')
2209 2207 if not lines[-1]:
2210 2208 del lines[-1]
2211 2209 displayer.flush(ctx)
2212 2210 edges = edgefn(type, char, lines, state, rev, parents)
2213 2211 for type, char, lines, coldata in edges:
2214 2212 graphmod.ascii(ui, state, type, char, lines, coldata)
2215 2213 displayer.close()
2216 2214
2217 2215 def graphlog(ui, repo, *pats, **opts):
2218 2216 # Parameters are identical to log command ones
2219 2217 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2220 2218 revdag = graphmod.dagwalker(repo, revs)
2221 2219
2222 2220 getrenamed = None
2223 2221 if opts.get('copies'):
2224 2222 endrev = None
2225 2223 if opts.get('rev'):
2226 2224 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2227 2225 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2228 2226
2229 2227 ui.pager('log')
2230 2228 displayer = show_changeset(ui, repo, opts, buffered=True)
2231 2229 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2232 2230 filematcher)
2233 2231
2234 2232 def checkunsupportedgraphflags(pats, opts):
2235 2233 for op in ["newest_first"]:
2236 2234 if op in opts and opts[op]:
2237 2235 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2238 2236 % op.replace("_", "-"))
2239 2237
2240 2238 def graphrevs(repo, nodes, opts):
2241 2239 limit = loglimit(opts)
2242 2240 nodes.reverse()
2243 2241 if limit is not None:
2244 2242 nodes = nodes[:limit]
2245 2243 return graphmod.nodes(repo, nodes)
2246 2244
2247 2245 def add(ui, repo, match, prefix, explicitonly, **opts):
2248 2246 join = lambda f: os.path.join(prefix, f)
2249 2247 bad = []
2250 2248
2251 2249 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2252 2250 names = []
2253 2251 wctx = repo[None]
2254 2252 cca = None
2255 2253 abort, warn = scmutil.checkportabilityalert(ui)
2256 2254 if abort or warn:
2257 2255 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2258 2256
2259 2257 badmatch = matchmod.badmatch(match, badfn)
2260 2258 dirstate = repo.dirstate
2261 2259 # We don't want to just call wctx.walk here, since it would return a lot of
2262 2260 # clean files, which we aren't interested in and takes time.
2263 2261 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2264 2262 True, False, full=False)):
2265 2263 exact = match.exact(f)
2266 2264 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2267 2265 if cca:
2268 2266 cca(f)
2269 2267 names.append(f)
2270 2268 if ui.verbose or not exact:
2271 2269 ui.status(_('adding %s\n') % match.rel(f))
2272 2270
2273 2271 for subpath in sorted(wctx.substate):
2274 2272 sub = wctx.sub(subpath)
2275 2273 try:
2276 2274 submatch = matchmod.subdirmatcher(subpath, match)
2277 2275 if opts.get('subrepos'):
2278 2276 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2279 2277 else:
2280 2278 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2281 2279 except error.LookupError:
2282 2280 ui.status(_("skipping missing subrepository: %s\n")
2283 2281 % join(subpath))
2284 2282
2285 2283 if not opts.get('dry_run'):
2286 2284 rejected = wctx.add(names, prefix)
2287 2285 bad.extend(f for f in rejected if f in match.files())
2288 2286 return bad
2289 2287
2290 2288 def forget(ui, repo, match, prefix, explicitonly):
2291 2289 join = lambda f: os.path.join(prefix, f)
2292 2290 bad = []
2293 2291 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2294 2292 wctx = repo[None]
2295 2293 forgot = []
2296 2294
2297 2295 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2298 2296 forget = sorted(s[0] + s[1] + s[3] + s[6])
2299 2297 if explicitonly:
2300 2298 forget = [f for f in forget if match.exact(f)]
2301 2299
2302 2300 for subpath in sorted(wctx.substate):
2303 2301 sub = wctx.sub(subpath)
2304 2302 try:
2305 2303 submatch = matchmod.subdirmatcher(subpath, match)
2306 2304 subbad, subforgot = sub.forget(submatch, prefix)
2307 2305 bad.extend([subpath + '/' + f for f in subbad])
2308 2306 forgot.extend([subpath + '/' + f for f in subforgot])
2309 2307 except error.LookupError:
2310 2308 ui.status(_("skipping missing subrepository: %s\n")
2311 2309 % join(subpath))
2312 2310
2313 2311 if not explicitonly:
2314 2312 for f in match.files():
2315 2313 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2316 2314 if f not in forgot:
2317 2315 if repo.wvfs.exists(f):
2318 2316 # Don't complain if the exact case match wasn't given.
2319 2317 # But don't do this until after checking 'forgot', so
2320 2318 # that subrepo files aren't normalized, and this op is
2321 2319 # purely from data cached by the status walk above.
2322 2320 if repo.dirstate.normalize(f) in repo.dirstate:
2323 2321 continue
2324 2322 ui.warn(_('not removing %s: '
2325 2323 'file is already untracked\n')
2326 2324 % match.rel(f))
2327 2325 bad.append(f)
2328 2326
2329 2327 for f in forget:
2330 2328 if ui.verbose or not match.exact(f):
2331 2329 ui.status(_('removing %s\n') % match.rel(f))
2332 2330
2333 2331 rejected = wctx.forget(forget, prefix)
2334 2332 bad.extend(f for f in rejected if f in match.files())
2335 2333 forgot.extend(f for f in forget if f not in rejected)
2336 2334 return bad, forgot
2337 2335
2338 2336 def files(ui, ctx, m, fm, fmt, subrepos):
2339 2337 rev = ctx.rev()
2340 2338 ret = 1
2341 2339 ds = ctx.repo().dirstate
2342 2340
2343 2341 for f in ctx.matches(m):
2344 2342 if rev is None and ds[f] == 'r':
2345 2343 continue
2346 2344 fm.startitem()
2347 2345 if ui.verbose:
2348 2346 fc = ctx[f]
2349 2347 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2350 2348 fm.data(abspath=f)
2351 2349 fm.write('path', fmt, m.rel(f))
2352 2350 ret = 0
2353 2351
2354 2352 for subpath in sorted(ctx.substate):
2355 2353 submatch = matchmod.subdirmatcher(subpath, m)
2356 2354 if (subrepos or m.exact(subpath) or any(submatch.files())):
2357 2355 sub = ctx.sub(subpath)
2358 2356 try:
2359 2357 recurse = m.exact(subpath) or subrepos
2360 2358 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2361 2359 ret = 0
2362 2360 except error.LookupError:
2363 2361 ui.status(_("skipping missing subrepository: %s\n")
2364 2362 % m.abs(subpath))
2365 2363
2366 2364 return ret
2367 2365
2368 2366 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2369 2367 join = lambda f: os.path.join(prefix, f)
2370 2368 ret = 0
2371 2369 s = repo.status(match=m, clean=True)
2372 2370 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2373 2371
2374 2372 wctx = repo[None]
2375 2373
2376 2374 if warnings is None:
2377 2375 warnings = []
2378 2376 warn = True
2379 2377 else:
2380 2378 warn = False
2381 2379
2382 2380 subs = sorted(wctx.substate)
2383 2381 total = len(subs)
2384 2382 count = 0
2385 2383 for subpath in subs:
2386 2384 count += 1
2387 2385 submatch = matchmod.subdirmatcher(subpath, m)
2388 2386 if subrepos or m.exact(subpath) or any(submatch.files()):
2389 2387 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2390 2388 sub = wctx.sub(subpath)
2391 2389 try:
2392 2390 if sub.removefiles(submatch, prefix, after, force, subrepos,
2393 2391 warnings):
2394 2392 ret = 1
2395 2393 except error.LookupError:
2396 2394 warnings.append(_("skipping missing subrepository: %s\n")
2397 2395 % join(subpath))
2398 2396 ui.progress(_('searching'), None)
2399 2397
2400 2398 # warn about failure to delete explicit files/dirs
2401 2399 deleteddirs = util.dirs(deleted)
2402 2400 files = m.files()
2403 2401 total = len(files)
2404 2402 count = 0
2405 2403 for f in files:
2406 2404 def insubrepo():
2407 2405 for subpath in wctx.substate:
2408 2406 if f.startswith(subpath + '/'):
2409 2407 return True
2410 2408 return False
2411 2409
2412 2410 count += 1
2413 2411 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2414 2412 isdir = f in deleteddirs or wctx.hasdir(f)
2415 2413 if (f in repo.dirstate or isdir or f == '.'
2416 2414 or insubrepo() or f in subs):
2417 2415 continue
2418 2416
2419 2417 if repo.wvfs.exists(f):
2420 2418 if repo.wvfs.isdir(f):
2421 2419 warnings.append(_('not removing %s: no tracked files\n')
2422 2420 % m.rel(f))
2423 2421 else:
2424 2422 warnings.append(_('not removing %s: file is untracked\n')
2425 2423 % m.rel(f))
2426 2424 # missing files will generate a warning elsewhere
2427 2425 ret = 1
2428 2426 ui.progress(_('deleting'), None)
2429 2427
2430 2428 if force:
2431 2429 list = modified + deleted + clean + added
2432 2430 elif after:
2433 2431 list = deleted
2434 2432 remaining = modified + added + clean
2435 2433 total = len(remaining)
2436 2434 count = 0
2437 2435 for f in remaining:
2438 2436 count += 1
2439 2437 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2440 2438 warnings.append(_('not removing %s: file still exists\n')
2441 2439 % m.rel(f))
2442 2440 ret = 1
2443 2441 ui.progress(_('skipping'), None)
2444 2442 else:
2445 2443 list = deleted + clean
2446 2444 total = len(modified) + len(added)
2447 2445 count = 0
2448 2446 for f in modified:
2449 2447 count += 1
2450 2448 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2451 2449 warnings.append(_('not removing %s: file is modified (use -f'
2452 2450 ' to force removal)\n') % m.rel(f))
2453 2451 ret = 1
2454 2452 for f in added:
2455 2453 count += 1
2456 2454 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2457 2455 warnings.append(_("not removing %s: file has been marked for add"
2458 2456 " (use 'hg forget' to undo add)\n") % m.rel(f))
2459 2457 ret = 1
2460 2458 ui.progress(_('skipping'), None)
2461 2459
2462 2460 list = sorted(list)
2463 2461 total = len(list)
2464 2462 count = 0
2465 2463 for f in list:
2466 2464 count += 1
2467 2465 if ui.verbose or not m.exact(f):
2468 2466 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2469 2467 ui.status(_('removing %s\n') % m.rel(f))
2470 2468 ui.progress(_('deleting'), None)
2471 2469
2472 2470 with repo.wlock():
2473 2471 if not after:
2474 2472 for f in list:
2475 2473 if f in added:
2476 2474 continue # we never unlink added files on remove
2477 2475 repo.wvfs.unlinkpath(f, ignoremissing=True)
2478 2476 repo[None].forget(list)
2479 2477
2480 2478 if warn:
2481 2479 for warning in warnings:
2482 2480 ui.warn(warning)
2483 2481
2484 2482 return ret
2485 2483
2486 2484 def cat(ui, repo, ctx, matcher, prefix, **opts):
2487 2485 err = 1
2488 2486
2489 2487 def write(path):
2490 2488 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2491 2489 pathname=os.path.join(prefix, path))
2492 2490 data = ctx[path].data()
2493 2491 if opts.get('decode'):
2494 2492 data = repo.wwritedata(path, data)
2495 2493 fp.write(data)
2496 2494 fp.close()
2497 2495
2498 2496 # Automation often uses hg cat on single files, so special case it
2499 2497 # for performance to avoid the cost of parsing the manifest.
2500 2498 if len(matcher.files()) == 1 and not matcher.anypats():
2501 2499 file = matcher.files()[0]
2502 2500 mfl = repo.manifestlog
2503 2501 mfnode = ctx.manifestnode()
2504 2502 try:
2505 2503 if mfnode and mfl[mfnode].find(file)[0]:
2506 2504 write(file)
2507 2505 return 0
2508 2506 except KeyError:
2509 2507 pass
2510 2508
2511 2509 for abs in ctx.walk(matcher):
2512 2510 write(abs)
2513 2511 err = 0
2514 2512
2515 2513 for subpath in sorted(ctx.substate):
2516 2514 sub = ctx.sub(subpath)
2517 2515 try:
2518 2516 submatch = matchmod.subdirmatcher(subpath, matcher)
2519 2517
2520 2518 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2521 2519 **opts):
2522 2520 err = 0
2523 2521 except error.RepoLookupError:
2524 2522 ui.status(_("skipping missing subrepository: %s\n")
2525 2523 % os.path.join(prefix, subpath))
2526 2524
2527 2525 return err
2528 2526
2529 2527 def commit(ui, repo, commitfunc, pats, opts):
2530 2528 '''commit the specified files or all outstanding changes'''
2531 2529 date = opts.get('date')
2532 2530 if date:
2533 2531 opts['date'] = util.parsedate(date)
2534 2532 message = logmessage(ui, opts)
2535 2533 matcher = scmutil.match(repo[None], pats, opts)
2536 2534
2537 2535 # extract addremove carefully -- this function can be called from a command
2538 2536 # that doesn't support addremove
2539 2537 if opts.get('addremove'):
2540 2538 if scmutil.addremove(repo, matcher, "", opts) != 0:
2541 2539 raise error.Abort(
2542 2540 _("failed to mark all new/missing files as added/removed"))
2543 2541
2544 2542 return commitfunc(ui, repo, message, matcher, opts)
2545 2543
2546 2544 def samefile(f, ctx1, ctx2):
2547 2545 if f in ctx1.manifest():
2548 2546 a = ctx1.filectx(f)
2549 2547 if f in ctx2.manifest():
2550 2548 b = ctx2.filectx(f)
2551 2549 return (not a.cmp(b)
2552 2550 and a.flags() == b.flags())
2553 2551 else:
2554 2552 return False
2555 2553 else:
2556 2554 return f not in ctx2.manifest()
2557 2555
2558 2556 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2559 2557 # avoid cycle context -> subrepo -> cmdutil
2560 2558 from . import context
2561 2559
2562 2560 # amend will reuse the existing user if not specified, but the obsolete
2563 2561 # marker creation requires that the current user's name is specified.
2564 2562 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2565 2563 ui.username() # raise exception if username not set
2566 2564
2567 2565 ui.note(_('amending changeset %s\n') % old)
2568 2566 base = old.p1()
2569 2567 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2570 2568
2571 2569 wlock = lock = newid = None
2572 2570 try:
2573 2571 wlock = repo.wlock()
2574 2572 lock = repo.lock()
2575 2573 with repo.transaction('amend') as tr:
2576 2574 # See if we got a message from -m or -l, if not, open the editor
2577 2575 # with the message of the changeset to amend
2578 2576 message = logmessage(ui, opts)
2579 2577 # ensure logfile does not conflict with later enforcement of the
2580 2578 # message. potential logfile content has been processed by
2581 2579 # `logmessage` anyway.
2582 2580 opts.pop('logfile')
2583 2581 # First, do a regular commit to record all changes in the working
2584 2582 # directory (if there are any)
2585 2583 ui.callhooks = False
2586 2584 activebookmark = repo._bookmarks.active
2587 2585 try:
2588 2586 repo._bookmarks.active = None
2589 2587 opts['message'] = 'temporary amend commit for %s' % old
2590 2588 node = commit(ui, repo, commitfunc, pats, opts)
2591 2589 finally:
2592 2590 repo._bookmarks.active = activebookmark
2593 2591 repo._bookmarks.recordchange(tr)
2594 2592 ui.callhooks = True
2595 2593 ctx = repo[node]
2596 2594
2597 2595 # Participating changesets:
2598 2596 #
2599 2597 # node/ctx o - new (intermediate) commit that contains changes
2600 2598 # | from working dir to go into amending commit
2601 2599 # | (or a workingctx if there were no changes)
2602 2600 # |
2603 2601 # old o - changeset to amend
2604 2602 # |
2605 2603 # base o - parent of amending changeset
2606 2604
2607 2605 # Update extra dict from amended commit (e.g. to preserve graft
2608 2606 # source)
2609 2607 extra.update(old.extra())
2610 2608
2611 2609 # Also update it from the intermediate commit or from the wctx
2612 2610 extra.update(ctx.extra())
2613 2611
2614 2612 if len(old.parents()) > 1:
2615 2613 # ctx.files() isn't reliable for merges, so fall back to the
2616 2614 # slower repo.status() method
2617 2615 files = set([fn for st in repo.status(base, old)[:3]
2618 2616 for fn in st])
2619 2617 else:
2620 2618 files = set(old.files())
2621 2619
2622 2620 # Second, we use either the commit we just did, or if there were no
2623 2621 # changes the parent of the working directory as the version of the
2624 2622 # files in the final amend commit
2625 2623 if node:
2626 2624 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2627 2625
2628 2626 user = ctx.user()
2629 2627 date = ctx.date()
2630 2628 # Recompute copies (avoid recording a -> b -> a)
2631 2629 copied = copies.pathcopies(base, ctx)
2632 2630 if old.p2:
2633 2631 copied.update(copies.pathcopies(old.p2(), ctx))
2634 2632
2635 2633 # Prune files which were reverted by the updates: if old
2636 2634 # introduced file X and our intermediate commit, node,
2637 2635 # renamed that file, then those two files are the same and
2638 2636 # we can discard X from our list of files. Likewise if X
2639 2637 # was deleted, it's no longer relevant
2640 2638 files.update(ctx.files())
2641 2639 files = [f for f in files if not samefile(f, ctx, base)]
2642 2640
2643 2641 def filectxfn(repo, ctx_, path):
2644 2642 try:
2645 2643 fctx = ctx[path]
2646 2644 flags = fctx.flags()
2647 2645 mctx = context.memfilectx(repo,
2648 2646 fctx.path(), fctx.data(),
2649 2647 islink='l' in flags,
2650 2648 isexec='x' in flags,
2651 2649 copied=copied.get(path))
2652 2650 return mctx
2653 2651 except KeyError:
2654 2652 return None
2655 2653 else:
2656 2654 ui.note(_('copying changeset %s to %s\n') % (old, base))
2657 2655
2658 2656 # Use version of files as in the old cset
2659 2657 def filectxfn(repo, ctx_, path):
2660 2658 try:
2661 2659 return old.filectx(path)
2662 2660 except KeyError:
2663 2661 return None
2664 2662
2665 2663 user = opts.get('user') or old.user()
2666 2664 date = opts.get('date') or old.date()
2667 2665 editform = mergeeditform(old, 'commit.amend')
2668 2666 editor = getcommiteditor(editform=editform, **opts)
2669 2667 if not message:
2670 2668 editor = getcommiteditor(edit=True, editform=editform)
2671 2669 message = old.description()
2672 2670
2673 2671 pureextra = extra.copy()
2674 2672 extra['amend_source'] = old.hex()
2675 2673
2676 2674 new = context.memctx(repo,
2677 2675 parents=[base.node(), old.p2().node()],
2678 2676 text=message,
2679 2677 files=files,
2680 2678 filectxfn=filectxfn,
2681 2679 user=user,
2682 2680 date=date,
2683 2681 extra=extra,
2684 2682 editor=editor)
2685 2683
2686 2684 newdesc = changelog.stripdesc(new.description())
2687 2685 if ((not node)
2688 2686 and newdesc == old.description()
2689 2687 and user == old.user()
2690 2688 and date == old.date()
2691 2689 and pureextra == old.extra()):
2692 2690 # nothing changed. continuing here would create a new node
2693 2691 # anyway because of the amend_source noise.
2694 2692 #
2695 2693 # This not what we expect from amend.
2696 2694 return old.node()
2697 2695
2698 2696 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2699 2697 try:
2700 2698 if opts.get('secret'):
2701 2699 commitphase = 'secret'
2702 2700 else:
2703 2701 commitphase = old.phase()
2704 2702 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2705 2703 newid = repo.commitctx(new)
2706 2704 finally:
2707 2705 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2708 2706 if newid != old.node():
2709 2707 # Reroute the working copy parent to the new changeset
2710 2708 repo.setparents(newid, nullid)
2711 2709
2712 2710 # Move bookmarks from old parent to amend commit
2713 2711 bms = repo.nodebookmarks(old.node())
2714 2712 if bms:
2715 2713 marks = repo._bookmarks
2716 2714 for bm in bms:
2717 2715 ui.debug('moving bookmarks %r from %s to %s\n' %
2718 2716 (marks, old.hex(), hex(newid)))
2719 2717 marks[bm] = newid
2720 2718 marks.recordchange(tr)
2721 2719 #commit the whole amend process
2722 2720 if createmarkers:
2723 2721 # mark the new changeset as successor of the rewritten one
2724 2722 new = repo[newid]
2725 2723 obs = [(old, (new,))]
2726 2724 if node:
2727 2725 obs.append((ctx, ()))
2728 2726
2729 2727 obsolete.createmarkers(repo, obs)
2730 2728 if not createmarkers and newid != old.node():
2731 2729 # Strip the intermediate commit (if there was one) and the amended
2732 2730 # commit
2733 2731 if node:
2734 2732 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2735 2733 ui.note(_('stripping amended changeset %s\n') % old)
2736 2734 repair.strip(ui, repo, old.node(), topic='amend-backup')
2737 2735 finally:
2738 2736 lockmod.release(lock, wlock)
2739 2737 return newid
2740 2738
2741 2739 def commiteditor(repo, ctx, subs, editform=''):
2742 2740 if ctx.description():
2743 2741 return ctx.description()
2744 2742 return commitforceeditor(repo, ctx, subs, editform=editform,
2745 2743 unchangedmessagedetection=True)
2746 2744
2747 2745 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2748 2746 editform='', unchangedmessagedetection=False):
2749 2747 if not extramsg:
2750 2748 extramsg = _("Leave message empty to abort commit.")
2751 2749
2752 2750 forms = [e for e in editform.split('.') if e]
2753 2751 forms.insert(0, 'changeset')
2754 2752 templatetext = None
2755 2753 while forms:
2756 2754 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2757 2755 if tmpl:
2758 2756 templatetext = committext = buildcommittemplate(
2759 2757 repo, ctx, subs, extramsg, tmpl)
2760 2758 break
2761 2759 forms.pop()
2762 2760 else:
2763 2761 committext = buildcommittext(repo, ctx, subs, extramsg)
2764 2762
2765 2763 # run editor in the repository root
2766 2764 olddir = pycompat.getcwd()
2767 2765 os.chdir(repo.root)
2768 2766
2769 2767 # make in-memory changes visible to external process
2770 2768 tr = repo.currenttransaction()
2771 2769 repo.dirstate.write(tr)
2772 2770 pending = tr and tr.writepending() and repo.root
2773 2771
2774 2772 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2775 2773 editform=editform, pending=pending,
2776 2774 repopath=repo.path)
2777 2775 text = editortext
2778 2776
2779 2777 # strip away anything below this special string (used for editors that want
2780 2778 # to display the diff)
2781 2779 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2782 2780 if stripbelow:
2783 2781 text = text[:stripbelow.start()]
2784 2782
2785 2783 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2786 2784 os.chdir(olddir)
2787 2785
2788 2786 if finishdesc:
2789 2787 text = finishdesc(text)
2790 2788 if not text.strip():
2791 2789 raise error.Abort(_("empty commit message"))
2792 2790 if unchangedmessagedetection and editortext == templatetext:
2793 2791 raise error.Abort(_("commit message unchanged"))
2794 2792
2795 2793 return text
2796 2794
2797 2795 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2798 2796 ui = repo.ui
2799 2797 tmpl, mapfile = gettemplate(ui, tmpl, None)
2800 2798
2801 2799 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2802 2800
2803 2801 for k, v in repo.ui.configitems('committemplate'):
2804 2802 if k != 'changeset':
2805 2803 t.t.cache[k] = v
2806 2804
2807 2805 if not extramsg:
2808 2806 extramsg = '' # ensure that extramsg is string
2809 2807
2810 2808 ui.pushbuffer()
2811 2809 t.show(ctx, extramsg=extramsg)
2812 2810 return ui.popbuffer()
2813 2811
2814 2812 def hgprefix(msg):
2815 2813 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2816 2814
2817 2815 def buildcommittext(repo, ctx, subs, extramsg):
2818 2816 edittext = []
2819 2817 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2820 2818 if ctx.description():
2821 2819 edittext.append(ctx.description())
2822 2820 edittext.append("")
2823 2821 edittext.append("") # Empty line between message and comments.
2824 2822 edittext.append(hgprefix(_("Enter commit message."
2825 2823 " Lines beginning with 'HG:' are removed.")))
2826 2824 edittext.append(hgprefix(extramsg))
2827 2825 edittext.append("HG: --")
2828 2826 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2829 2827 if ctx.p2():
2830 2828 edittext.append(hgprefix(_("branch merge")))
2831 2829 if ctx.branch():
2832 2830 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2833 2831 if bookmarks.isactivewdirparent(repo):
2834 2832 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2835 2833 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2836 2834 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2837 2835 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2838 2836 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2839 2837 if not added and not modified and not removed:
2840 2838 edittext.append(hgprefix(_("no files changed")))
2841 2839 edittext.append("")
2842 2840
2843 2841 return "\n".join(edittext)
2844 2842
2845 2843 def commitstatus(repo, node, branch, bheads=None, opts=None):
2846 2844 if opts is None:
2847 2845 opts = {}
2848 2846 ctx = repo[node]
2849 2847 parents = ctx.parents()
2850 2848
2851 2849 if (not opts.get('amend') and bheads and node not in bheads and not
2852 2850 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2853 2851 repo.ui.status(_('created new head\n'))
2854 2852 # The message is not printed for initial roots. For the other
2855 2853 # changesets, it is printed in the following situations:
2856 2854 #
2857 2855 # Par column: for the 2 parents with ...
2858 2856 # N: null or no parent
2859 2857 # B: parent is on another named branch
2860 2858 # C: parent is a regular non head changeset
2861 2859 # H: parent was a branch head of the current branch
2862 2860 # Msg column: whether we print "created new head" message
2863 2861 # In the following, it is assumed that there already exists some
2864 2862 # initial branch heads of the current branch, otherwise nothing is
2865 2863 # printed anyway.
2866 2864 #
2867 2865 # Par Msg Comment
2868 2866 # N N y additional topo root
2869 2867 #
2870 2868 # B N y additional branch root
2871 2869 # C N y additional topo head
2872 2870 # H N n usual case
2873 2871 #
2874 2872 # B B y weird additional branch root
2875 2873 # C B y branch merge
2876 2874 # H B n merge with named branch
2877 2875 #
2878 2876 # C C y additional head from merge
2879 2877 # C H n merge with a head
2880 2878 #
2881 2879 # H H n head merge: head count decreases
2882 2880
2883 2881 if not opts.get('close_branch'):
2884 2882 for r in parents:
2885 2883 if r.closesbranch() and r.branch() == branch:
2886 2884 repo.ui.status(_('reopening closed branch head %d\n') % r)
2887 2885
2888 2886 if repo.ui.debugflag:
2889 2887 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2890 2888 elif repo.ui.verbose:
2891 2889 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2892 2890
2893 2891 def postcommitstatus(repo, pats, opts):
2894 2892 return repo.status(match=scmutil.match(repo[None], pats, opts))
2895 2893
2896 2894 def revert(ui, repo, ctx, parents, *pats, **opts):
2897 2895 parent, p2 = parents
2898 2896 node = ctx.node()
2899 2897
2900 2898 mf = ctx.manifest()
2901 2899 if node == p2:
2902 2900 parent = p2
2903 2901
2904 2902 # need all matching names in dirstate and manifest of target rev,
2905 2903 # so have to walk both. do not print errors if files exist in one
2906 2904 # but not other. in both cases, filesets should be evaluated against
2907 2905 # workingctx to get consistent result (issue4497). this means 'set:**'
2908 2906 # cannot be used to select missing files from target rev.
2909 2907
2910 2908 # `names` is a mapping for all elements in working copy and target revision
2911 2909 # The mapping is in the form:
2912 2910 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2913 2911 names = {}
2914 2912
2915 2913 with repo.wlock():
2916 2914 ## filling of the `names` mapping
2917 2915 # walk dirstate to fill `names`
2918 2916
2919 2917 interactive = opts.get('interactive', False)
2920 2918 wctx = repo[None]
2921 2919 m = scmutil.match(wctx, pats, opts)
2922 2920
2923 2921 # we'll need this later
2924 2922 targetsubs = sorted(s for s in wctx.substate if m(s))
2925 2923
2926 2924 if not m.always():
2927 2925 for abs in repo.walk(matchmod.badmatch(m, lambda x, y: False)):
2928 2926 names[abs] = m.rel(abs), m.exact(abs)
2929 2927
2930 2928 # walk target manifest to fill `names`
2931 2929
2932 2930 def badfn(path, msg):
2933 2931 if path in names:
2934 2932 return
2935 2933 if path in ctx.substate:
2936 2934 return
2937 2935 path_ = path + '/'
2938 2936 for f in names:
2939 2937 if f.startswith(path_):
2940 2938 return
2941 2939 ui.warn("%s: %s\n" % (m.rel(path), msg))
2942 2940
2943 2941 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2944 2942 if abs not in names:
2945 2943 names[abs] = m.rel(abs), m.exact(abs)
2946 2944
2947 2945 # Find status of all file in `names`.
2948 2946 m = scmutil.matchfiles(repo, names)
2949 2947
2950 2948 changes = repo.status(node1=node, match=m,
2951 2949 unknown=True, ignored=True, clean=True)
2952 2950 else:
2953 2951 changes = repo.status(node1=node, match=m)
2954 2952 for kind in changes:
2955 2953 for abs in kind:
2956 2954 names[abs] = m.rel(abs), m.exact(abs)
2957 2955
2958 2956 m = scmutil.matchfiles(repo, names)
2959 2957
2960 2958 modified = set(changes.modified)
2961 2959 added = set(changes.added)
2962 2960 removed = set(changes.removed)
2963 2961 _deleted = set(changes.deleted)
2964 2962 unknown = set(changes.unknown)
2965 2963 unknown.update(changes.ignored)
2966 2964 clean = set(changes.clean)
2967 2965 modadded = set()
2968 2966
2969 2967 # We need to account for the state of the file in the dirstate,
2970 2968 # even when we revert against something else than parent. This will
2971 2969 # slightly alter the behavior of revert (doing back up or not, delete
2972 2970 # or just forget etc).
2973 2971 if parent == node:
2974 2972 dsmodified = modified
2975 2973 dsadded = added
2976 2974 dsremoved = removed
2977 2975 # store all local modifications, useful later for rename detection
2978 2976 localchanges = dsmodified | dsadded
2979 2977 modified, added, removed = set(), set(), set()
2980 2978 else:
2981 2979 changes = repo.status(node1=parent, match=m)
2982 2980 dsmodified = set(changes.modified)
2983 2981 dsadded = set(changes.added)
2984 2982 dsremoved = set(changes.removed)
2985 2983 # store all local modifications, useful later for rename detection
2986 2984 localchanges = dsmodified | dsadded
2987 2985
2988 2986 # only take into account for removes between wc and target
2989 2987 clean |= dsremoved - removed
2990 2988 dsremoved &= removed
2991 2989 # distinct between dirstate remove and other
2992 2990 removed -= dsremoved
2993 2991
2994 2992 modadded = added & dsmodified
2995 2993 added -= modadded
2996 2994
2997 2995 # tell newly modified apart.
2998 2996 dsmodified &= modified
2999 2997 dsmodified |= modified & dsadded # dirstate added may need backup
3000 2998 modified -= dsmodified
3001 2999
3002 3000 # We need to wait for some post-processing to update this set
3003 3001 # before making the distinction. The dirstate will be used for
3004 3002 # that purpose.
3005 3003 dsadded = added
3006 3004
3007 3005 # in case of merge, files that are actually added can be reported as
3008 3006 # modified, we need to post process the result
3009 3007 if p2 != nullid:
3010 3008 mergeadd = set(dsmodified)
3011 3009 for path in dsmodified:
3012 3010 if path in mf:
3013 3011 mergeadd.remove(path)
3014 3012 dsadded |= mergeadd
3015 3013 dsmodified -= mergeadd
3016 3014
3017 3015 # if f is a rename, update `names` to also revert the source
3018 3016 cwd = repo.getcwd()
3019 3017 for f in localchanges:
3020 3018 src = repo.dirstate.copied(f)
3021 3019 # XXX should we check for rename down to target node?
3022 3020 if src and src not in names and repo.dirstate[src] == 'r':
3023 3021 dsremoved.add(src)
3024 3022 names[src] = (repo.pathto(src, cwd), True)
3025 3023
3026 3024 # determine the exact nature of the deleted changesets
3027 3025 deladded = set(_deleted)
3028 3026 for path in _deleted:
3029 3027 if path in mf:
3030 3028 deladded.remove(path)
3031 3029 deleted = _deleted - deladded
3032 3030
3033 3031 # distinguish between file to forget and the other
3034 3032 added = set()
3035 3033 for abs in dsadded:
3036 3034 if repo.dirstate[abs] != 'a':
3037 3035 added.add(abs)
3038 3036 dsadded -= added
3039 3037
3040 3038 for abs in deladded:
3041 3039 if repo.dirstate[abs] == 'a':
3042 3040 dsadded.add(abs)
3043 3041 deladded -= dsadded
3044 3042
3045 3043 # For files marked as removed, we check if an unknown file is present at
3046 3044 # the same path. If a such file exists it may need to be backed up.
3047 3045 # Making the distinction at this stage helps have simpler backup
3048 3046 # logic.
3049 3047 removunk = set()
3050 3048 for abs in removed:
3051 3049 target = repo.wjoin(abs)
3052 3050 if os.path.lexists(target):
3053 3051 removunk.add(abs)
3054 3052 removed -= removunk
3055 3053
3056 3054 dsremovunk = set()
3057 3055 for abs in dsremoved:
3058 3056 target = repo.wjoin(abs)
3059 3057 if os.path.lexists(target):
3060 3058 dsremovunk.add(abs)
3061 3059 dsremoved -= dsremovunk
3062 3060
3063 3061 # action to be actually performed by revert
3064 3062 # (<list of file>, message>) tuple
3065 3063 actions = {'revert': ([], _('reverting %s\n')),
3066 3064 'add': ([], _('adding %s\n')),
3067 3065 'remove': ([], _('removing %s\n')),
3068 3066 'drop': ([], _('removing %s\n')),
3069 3067 'forget': ([], _('forgetting %s\n')),
3070 3068 'undelete': ([], _('undeleting %s\n')),
3071 3069 'noop': (None, _('no changes needed to %s\n')),
3072 3070 'unknown': (None, _('file not managed: %s\n')),
3073 3071 }
3074 3072
3075 3073 # "constant" that convey the backup strategy.
3076 3074 # All set to `discard` if `no-backup` is set do avoid checking
3077 3075 # no_backup lower in the code.
3078 3076 # These values are ordered for comparison purposes
3079 3077 backupinteractive = 3 # do backup if interactively modified
3080 3078 backup = 2 # unconditionally do backup
3081 3079 check = 1 # check if the existing file differs from target
3082 3080 discard = 0 # never do backup
3083 3081 if opts.get('no_backup'):
3084 3082 backupinteractive = backup = check = discard
3085 3083 if interactive:
3086 3084 dsmodifiedbackup = backupinteractive
3087 3085 else:
3088 3086 dsmodifiedbackup = backup
3089 3087 tobackup = set()
3090 3088
3091 3089 backupanddel = actions['remove']
3092 3090 if not opts.get('no_backup'):
3093 3091 backupanddel = actions['drop']
3094 3092
3095 3093 disptable = (
3096 3094 # dispatch table:
3097 3095 # file state
3098 3096 # action
3099 3097 # make backup
3100 3098
3101 3099 ## Sets that results that will change file on disk
3102 3100 # Modified compared to target, no local change
3103 3101 (modified, actions['revert'], discard),
3104 3102 # Modified compared to target, but local file is deleted
3105 3103 (deleted, actions['revert'], discard),
3106 3104 # Modified compared to target, local change
3107 3105 (dsmodified, actions['revert'], dsmodifiedbackup),
3108 3106 # Added since target
3109 3107 (added, actions['remove'], discard),
3110 3108 # Added in working directory
3111 3109 (dsadded, actions['forget'], discard),
3112 3110 # Added since target, have local modification
3113 3111 (modadded, backupanddel, backup),
3114 3112 # Added since target but file is missing in working directory
3115 3113 (deladded, actions['drop'], discard),
3116 3114 # Removed since target, before working copy parent
3117 3115 (removed, actions['add'], discard),
3118 3116 # Same as `removed` but an unknown file exists at the same path
3119 3117 (removunk, actions['add'], check),
3120 3118 # Removed since targe, marked as such in working copy parent
3121 3119 (dsremoved, actions['undelete'], discard),
3122 3120 # Same as `dsremoved` but an unknown file exists at the same path
3123 3121 (dsremovunk, actions['undelete'], check),
3124 3122 ## the following sets does not result in any file changes
3125 3123 # File with no modification
3126 3124 (clean, actions['noop'], discard),
3127 3125 # Existing file, not tracked anywhere
3128 3126 (unknown, actions['unknown'], discard),
3129 3127 )
3130 3128
3131 3129 for abs, (rel, exact) in sorted(names.items()):
3132 3130 # target file to be touch on disk (relative to cwd)
3133 3131 target = repo.wjoin(abs)
3134 3132 # search the entry in the dispatch table.
3135 3133 # if the file is in any of these sets, it was touched in the working
3136 3134 # directory parent and we are sure it needs to be reverted.
3137 3135 for table, (xlist, msg), dobackup in disptable:
3138 3136 if abs not in table:
3139 3137 continue
3140 3138 if xlist is not None:
3141 3139 xlist.append(abs)
3142 3140 if dobackup:
3143 3141 # If in interactive mode, don't automatically create
3144 3142 # .orig files (issue4793)
3145 3143 if dobackup == backupinteractive:
3146 3144 tobackup.add(abs)
3147 3145 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3148 3146 bakname = scmutil.origpath(ui, repo, rel)
3149 3147 ui.note(_('saving current version of %s as %s\n') %
3150 3148 (rel, bakname))
3151 3149 if not opts.get('dry_run'):
3152 3150 if interactive:
3153 3151 util.copyfile(target, bakname)
3154 3152 else:
3155 3153 util.rename(target, bakname)
3156 3154 if ui.verbose or not exact:
3157 3155 if not isinstance(msg, basestring):
3158 3156 msg = msg(abs)
3159 3157 ui.status(msg % rel)
3160 3158 elif exact:
3161 3159 ui.warn(msg % rel)
3162 3160 break
3163 3161
3164 3162 if not opts.get('dry_run'):
3165 3163 needdata = ('revert', 'add', 'undelete')
3166 3164 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3167 3165 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3168 3166
3169 3167 if targetsubs:
3170 3168 # Revert the subrepos on the revert list
3171 3169 for sub in targetsubs:
3172 3170 try:
3173 3171 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3174 3172 except KeyError:
3175 3173 raise error.Abort("subrepository '%s' does not exist in %s!"
3176 3174 % (sub, short(ctx.node())))
3177 3175
3178 3176 def _revertprefetch(repo, ctx, *files):
3179 3177 """Let extension changing the storage layer prefetch content"""
3180 3178 pass
3181 3179
3182 3180 def _performrevert(repo, parents, ctx, actions, interactive=False,
3183 3181 tobackup=None):
3184 3182 """function that actually perform all the actions computed for revert
3185 3183
3186 3184 This is an independent function to let extension to plug in and react to
3187 3185 the imminent revert.
3188 3186
3189 3187 Make sure you have the working directory locked when calling this function.
3190 3188 """
3191 3189 parent, p2 = parents
3192 3190 node = ctx.node()
3193 3191 excluded_files = []
3194 3192 matcher_opts = {"exclude": excluded_files}
3195 3193
3196 3194 def checkout(f):
3197 3195 fc = ctx[f]
3198 3196 repo.wwrite(f, fc.data(), fc.flags())
3199 3197
3200 3198 def doremove(f):
3201 3199 try:
3202 3200 repo.wvfs.unlinkpath(f)
3203 3201 except OSError:
3204 3202 pass
3205 3203 repo.dirstate.remove(f)
3206 3204
3207 3205 audit_path = pathutil.pathauditor(repo.root)
3208 3206 for f in actions['forget'][0]:
3209 3207 if interactive:
3210 3208 choice = repo.ui.promptchoice(
3211 3209 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3212 3210 if choice == 0:
3213 3211 repo.dirstate.drop(f)
3214 3212 else:
3215 3213 excluded_files.append(repo.wjoin(f))
3216 3214 else:
3217 3215 repo.dirstate.drop(f)
3218 3216 for f in actions['remove'][0]:
3219 3217 audit_path(f)
3220 3218 if interactive:
3221 3219 choice = repo.ui.promptchoice(
3222 3220 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3223 3221 if choice == 0:
3224 3222 doremove(f)
3225 3223 else:
3226 3224 excluded_files.append(repo.wjoin(f))
3227 3225 else:
3228 3226 doremove(f)
3229 3227 for f in actions['drop'][0]:
3230 3228 audit_path(f)
3231 3229 repo.dirstate.remove(f)
3232 3230
3233 3231 normal = None
3234 3232 if node == parent:
3235 3233 # We're reverting to our parent. If possible, we'd like status
3236 3234 # to report the file as clean. We have to use normallookup for
3237 3235 # merges to avoid losing information about merged/dirty files.
3238 3236 if p2 != nullid:
3239 3237 normal = repo.dirstate.normallookup
3240 3238 else:
3241 3239 normal = repo.dirstate.normal
3242 3240
3243 3241 newlyaddedandmodifiedfiles = set()
3244 3242 if interactive:
3245 3243 # Prompt the user for changes to revert
3246 3244 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3247 3245 m = scmutil.match(ctx, torevert, matcher_opts)
3248 3246 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3249 3247 diffopts.nodates = True
3250 3248 diffopts.git = True
3251 3249 operation = 'discard'
3252 3250 reversehunks = True
3253 3251 if node != parent:
3254 3252 operation = 'revert'
3255 3253 reversehunks = repo.ui.configbool('experimental',
3256 3254 'revertalternateinteractivemode',
3257 3255 True)
3258 3256 if reversehunks:
3259 3257 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3260 3258 else:
3261 3259 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3262 3260 originalchunks = patch.parsepatch(diff)
3263 3261
3264 3262 try:
3265 3263
3266 3264 chunks, opts = recordfilter(repo.ui, originalchunks,
3267 3265 operation=operation)
3268 3266 if reversehunks:
3269 3267 chunks = patch.reversehunks(chunks)
3270 3268
3271 3269 except patch.PatchError as err:
3272 3270 raise error.Abort(_('error parsing patch: %s') % err)
3273 3271
3274 3272 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3275 3273 if tobackup is None:
3276 3274 tobackup = set()
3277 3275 # Apply changes
3278 3276 fp = stringio()
3279 3277 for c in chunks:
3280 3278 # Create a backup file only if this hunk should be backed up
3281 3279 if ishunk(c) and c.header.filename() in tobackup:
3282 3280 abs = c.header.filename()
3283 3281 target = repo.wjoin(abs)
3284 3282 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3285 3283 util.copyfile(target, bakname)
3286 3284 tobackup.remove(abs)
3287 3285 c.write(fp)
3288 3286 dopatch = fp.tell()
3289 3287 fp.seek(0)
3290 3288 if dopatch:
3291 3289 try:
3292 3290 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3293 3291 except patch.PatchError as err:
3294 3292 raise error.Abort(str(err))
3295 3293 del fp
3296 3294 else:
3297 3295 for f in actions['revert'][0]:
3298 3296 checkout(f)
3299 3297 if normal:
3300 3298 normal(f)
3301 3299
3302 3300 for f in actions['add'][0]:
3303 3301 # Don't checkout modified files, they are already created by the diff
3304 3302 if f not in newlyaddedandmodifiedfiles:
3305 3303 checkout(f)
3306 3304 repo.dirstate.add(f)
3307 3305
3308 3306 normal = repo.dirstate.normallookup
3309 3307 if node == parent and p2 == nullid:
3310 3308 normal = repo.dirstate.normal
3311 3309 for f in actions['undelete'][0]:
3312 3310 checkout(f)
3313 3311 normal(f)
3314 3312
3315 3313 copied = copies.pathcopies(repo[parent], ctx)
3316 3314
3317 3315 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3318 3316 if f in copied:
3319 3317 repo.dirstate.copy(copied[f], f)
3320 3318
3321 3319 def command(table):
3322 3320 """Returns a function object to be used as a decorator for making commands.
3323 3321
3324 3322 This function receives a command table as its argument. The table should
3325 3323 be a dict.
3326 3324
3327 3325 The returned function can be used as a decorator for adding commands
3328 3326 to that command table. This function accepts multiple arguments to define
3329 3327 a command.
3330 3328
3331 3329 The first argument is the command name.
3332 3330
3333 3331 The options argument is an iterable of tuples defining command arguments.
3334 3332 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3335 3333
3336 3334 The synopsis argument defines a short, one line summary of how to use the
3337 3335 command. This shows up in the help output.
3338 3336
3339 3337 The norepo argument defines whether the command does not require a
3340 3338 local repository. Most commands operate against a repository, thus the
3341 3339 default is False.
3342 3340
3343 3341 The optionalrepo argument defines whether the command optionally requires
3344 3342 a local repository.
3345 3343
3346 3344 The inferrepo argument defines whether to try to find a repository from the
3347 3345 command line arguments. If True, arguments will be examined for potential
3348 3346 repository locations. See ``findrepo()``. If a repository is found, it
3349 3347 will be used.
3350 3348 """
3351 3349 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3352 3350 inferrepo=False):
3353 3351 def decorator(func):
3354 3352 func.norepo = norepo
3355 3353 func.optionalrepo = optionalrepo
3356 3354 func.inferrepo = inferrepo
3357 3355 if synopsis:
3358 3356 table[name] = func, list(options), synopsis
3359 3357 else:
3360 3358 table[name] = func, list(options)
3361 3359 return func
3362 3360 return decorator
3363 3361
3364 3362 return cmd
3365 3363
3366 3364 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3367 3365 # commands.outgoing. "missing" is "missing" of the result of
3368 3366 # "findcommonoutgoing()"
3369 3367 outgoinghooks = util.hooks()
3370 3368
3371 3369 # a list of (ui, repo) functions called by commands.summary
3372 3370 summaryhooks = util.hooks()
3373 3371
3374 3372 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3375 3373 #
3376 3374 # functions should return tuple of booleans below, if 'changes' is None:
3377 3375 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3378 3376 #
3379 3377 # otherwise, 'changes' is a tuple of tuples below:
3380 3378 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3381 3379 # - (desturl, destbranch, destpeer, outgoing)
3382 3380 summaryremotehooks = util.hooks()
3383 3381
3384 3382 # A list of state files kept by multistep operations like graft.
3385 3383 # Since graft cannot be aborted, it is considered 'clearable' by update.
3386 3384 # note: bisect is intentionally excluded
3387 3385 # (state file, clearable, allowcommit, error, hint)
3388 3386 unfinishedstates = [
3389 3387 ('graftstate', True, False, _('graft in progress'),
3390 3388 _("use 'hg graft --continue' or 'hg update' to abort")),
3391 3389 ('updatestate', True, False, _('last update was interrupted'),
3392 3390 _("use 'hg update' to get a consistent checkout"))
3393 3391 ]
3394 3392
3395 3393 def checkunfinished(repo, commit=False):
3396 3394 '''Look for an unfinished multistep operation, like graft, and abort
3397 3395 if found. It's probably good to check this right before
3398 3396 bailifchanged().
3399 3397 '''
3400 3398 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3401 3399 if commit and allowcommit:
3402 3400 continue
3403 3401 if repo.vfs.exists(f):
3404 3402 raise error.Abort(msg, hint=hint)
3405 3403
3406 3404 def clearunfinished(repo):
3407 3405 '''Check for unfinished operations (as above), and clear the ones
3408 3406 that are clearable.
3409 3407 '''
3410 3408 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3411 3409 if not clearable and repo.vfs.exists(f):
3412 3410 raise error.Abort(msg, hint=hint)
3413 3411 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3414 3412 if clearable and repo.vfs.exists(f):
3415 3413 util.unlink(repo.vfs.join(f))
3416 3414
3417 3415 afterresolvedstates = [
3418 3416 ('graftstate',
3419 3417 _('hg graft --continue')),
3420 3418 ]
3421 3419
3422 3420 def howtocontinue(repo):
3423 3421 '''Check for an unfinished operation and return the command to finish
3424 3422 it.
3425 3423
3426 3424 afterresolvedstates tuples define a .hg/{file} and the corresponding
3427 3425 command needed to finish it.
3428 3426
3429 3427 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3430 3428 a boolean.
3431 3429 '''
3432 3430 contmsg = _("continue: %s")
3433 3431 for f, msg in afterresolvedstates:
3434 3432 if repo.vfs.exists(f):
3435 3433 return contmsg % msg, True
3436 3434 workingctx = repo[None]
3437 3435 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3438 3436 for s in workingctx.substate)
3439 3437 if dirty:
3440 3438 return contmsg % _("hg commit"), False
3441 3439 return None, None
3442 3440
3443 3441 def checkafterresolved(repo):
3444 3442 '''Inform the user about the next action after completing hg resolve
3445 3443
3446 3444 If there's a matching afterresolvedstates, howtocontinue will yield
3447 3445 repo.ui.warn as the reporter.
3448 3446
3449 3447 Otherwise, it will yield repo.ui.note.
3450 3448 '''
3451 3449 msg, warning = howtocontinue(repo)
3452 3450 if msg is not None:
3453 3451 if warning:
3454 3452 repo.ui.warn("%s\n" % msg)
3455 3453 else:
3456 3454 repo.ui.note("%s\n" % msg)
3457 3455
3458 3456 def wrongtooltocontinue(repo, task):
3459 3457 '''Raise an abort suggesting how to properly continue if there is an
3460 3458 active task.
3461 3459
3462 3460 Uses howtocontinue() to find the active task.
3463 3461
3464 3462 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3465 3463 a hint.
3466 3464 '''
3467 3465 after = howtocontinue(repo)
3468 3466 hint = None
3469 3467 if after[1]:
3470 3468 hint = after[0]
3471 3469 raise error.Abort(_('no %s in progress') % task, hint=hint)
General Comments 0
You need to be logged in to leave comments. Login now