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