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