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