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