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