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