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