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