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