##// END OF EJS Templates
merge with stable
Matt Mackall -
r19130:70f0d1da merge default
parent child Browse files
Show More
@@ -1,2078 +1,2084 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, tempfile
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 13 import subrepo, context, repair, graphmod, revset, phases, obsolete
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(_("outstanding 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 loglimit(opts):
113 113 """get the log limit according to option -l/--limit"""
114 114 limit = opts.get('limit')
115 115 if limit:
116 116 try:
117 117 limit = int(limit)
118 118 except ValueError:
119 119 raise util.Abort(_('limit must be a positive integer'))
120 120 if limit <= 0:
121 121 raise util.Abort(_('limit must be positive'))
122 122 else:
123 123 limit = None
124 124 return limit
125 125
126 126 def makefilename(repo, pat, node, desc=None,
127 127 total=None, seqno=None, revwidth=None, pathname=None):
128 128 node_expander = {
129 129 'H': lambda: hex(node),
130 130 'R': lambda: str(repo.changelog.rev(node)),
131 131 'h': lambda: short(node),
132 132 'm': lambda: re.sub('[^\w]', '_', str(desc))
133 133 }
134 134 expander = {
135 135 '%': lambda: '%',
136 136 'b': lambda: os.path.basename(repo.root),
137 137 }
138 138
139 139 try:
140 140 if node:
141 141 expander.update(node_expander)
142 142 if node:
143 143 expander['r'] = (lambda:
144 144 str(repo.changelog.rev(node)).zfill(revwidth or 0))
145 145 if total is not None:
146 146 expander['N'] = lambda: str(total)
147 147 if seqno is not None:
148 148 expander['n'] = lambda: str(seqno)
149 149 if total is not None and seqno is not None:
150 150 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
151 151 if pathname is not None:
152 152 expander['s'] = lambda: os.path.basename(pathname)
153 153 expander['d'] = lambda: os.path.dirname(pathname) or '.'
154 154 expander['p'] = lambda: pathname
155 155
156 156 newname = []
157 157 patlen = len(pat)
158 158 i = 0
159 159 while i < patlen:
160 160 c = pat[i]
161 161 if c == '%':
162 162 i += 1
163 163 c = pat[i]
164 164 c = expander[c]()
165 165 newname.append(c)
166 166 i += 1
167 167 return ''.join(newname)
168 168 except KeyError, inst:
169 169 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
170 170 inst.args[0])
171 171
172 172 def makefileobj(repo, pat, node=None, desc=None, total=None,
173 173 seqno=None, revwidth=None, mode='wb', modemap={},
174 174 pathname=None):
175 175
176 176 writable = mode not in ('r', 'rb')
177 177
178 178 if not pat or pat == '-':
179 179 fp = writable and repo.ui.fout or repo.ui.fin
180 180 if util.safehasattr(fp, 'fileno'):
181 181 return os.fdopen(os.dup(fp.fileno()), mode)
182 182 else:
183 183 # if this fp can't be duped properly, return
184 184 # a dummy object that can be closed
185 185 class wrappedfileobj(object):
186 186 noop = lambda x: None
187 187 def __init__(self, f):
188 188 self.f = f
189 189 def __getattr__(self, attr):
190 190 if attr == 'close':
191 191 return self.noop
192 192 else:
193 193 return getattr(self.f, attr)
194 194
195 195 return wrappedfileobj(fp)
196 196 if util.safehasattr(pat, 'write') and writable:
197 197 return pat
198 198 if util.safehasattr(pat, 'read') and 'r' in mode:
199 199 return pat
200 200 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
201 201 mode = modemap.get(fn, mode)
202 202 if mode == 'wb':
203 203 modemap[fn] = 'ab'
204 204 return open(fn, mode)
205 205
206 206 def openrevlog(repo, cmd, file_, opts):
207 207 """opens the changelog, manifest, a filelog or a given revlog"""
208 208 cl = opts['changelog']
209 209 mf = opts['manifest']
210 210 msg = None
211 211 if cl and mf:
212 212 msg = _('cannot specify --changelog and --manifest at the same time')
213 213 elif cl or mf:
214 214 if file_:
215 215 msg = _('cannot specify filename with --changelog or --manifest')
216 216 elif not repo:
217 217 msg = _('cannot specify --changelog or --manifest '
218 218 'without a repository')
219 219 if msg:
220 220 raise util.Abort(msg)
221 221
222 222 r = None
223 223 if repo:
224 224 if cl:
225 225 r = repo.changelog
226 226 elif mf:
227 227 r = repo.manifest
228 228 elif file_:
229 229 filelog = repo.file(file_)
230 230 if len(filelog):
231 231 r = filelog
232 232 if not r:
233 233 if not file_:
234 234 raise error.CommandError(cmd, _('invalid arguments'))
235 235 if not os.path.isfile(file_):
236 236 raise util.Abort(_("revlog '%s' not found") % file_)
237 237 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
238 238 file_[:-2] + ".i")
239 239 return r
240 240
241 241 def copy(ui, repo, pats, opts, rename=False):
242 242 # called with the repo lock held
243 243 #
244 244 # hgsep => pathname that uses "/" to separate directories
245 245 # ossep => pathname that uses os.sep to separate directories
246 246 cwd = repo.getcwd()
247 247 targets = {}
248 248 after = opts.get("after")
249 249 dryrun = opts.get("dry_run")
250 250 wctx = repo[None]
251 251
252 252 def walkpat(pat):
253 253 srcs = []
254 254 badstates = after and '?' or '?r'
255 255 m = scmutil.match(repo[None], [pat], opts, globbed=True)
256 256 for abs in repo.walk(m):
257 257 state = repo.dirstate[abs]
258 258 rel = m.rel(abs)
259 259 exact = m.exact(abs)
260 260 if state in badstates:
261 261 if exact and state == '?':
262 262 ui.warn(_('%s: not copying - file is not managed\n') % rel)
263 263 if exact and state == 'r':
264 264 ui.warn(_('%s: not copying - file has been marked for'
265 265 ' remove\n') % rel)
266 266 continue
267 267 # abs: hgsep
268 268 # rel: ossep
269 269 srcs.append((abs, rel, exact))
270 270 return srcs
271 271
272 272 # abssrc: hgsep
273 273 # relsrc: ossep
274 274 # otarget: ossep
275 275 def copyfile(abssrc, relsrc, otarget, exact):
276 276 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
277 277 if '/' in abstarget:
278 278 # We cannot normalize abstarget itself, this would prevent
279 279 # case only renames, like a => A.
280 280 abspath, absname = abstarget.rsplit('/', 1)
281 281 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
282 282 reltarget = repo.pathto(abstarget, cwd)
283 283 target = repo.wjoin(abstarget)
284 284 src = repo.wjoin(abssrc)
285 285 state = repo.dirstate[abstarget]
286 286
287 287 scmutil.checkportable(ui, abstarget)
288 288
289 289 # check for collisions
290 290 prevsrc = targets.get(abstarget)
291 291 if prevsrc is not None:
292 292 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
293 293 (reltarget, repo.pathto(abssrc, cwd),
294 294 repo.pathto(prevsrc, cwd)))
295 295 return
296 296
297 297 # check for overwrites
298 298 exists = os.path.lexists(target)
299 299 samefile = False
300 300 if exists and abssrc != abstarget:
301 301 if (repo.dirstate.normalize(abssrc) ==
302 302 repo.dirstate.normalize(abstarget)):
303 303 if not rename:
304 304 ui.warn(_("%s: can't copy - same file\n") % reltarget)
305 305 return
306 306 exists = False
307 307 samefile = True
308 308
309 309 if not after and exists or after and state in 'mn':
310 310 if not opts['force']:
311 311 ui.warn(_('%s: not overwriting - file exists\n') %
312 312 reltarget)
313 313 return
314 314
315 315 if after:
316 316 if not exists:
317 317 if rename:
318 318 ui.warn(_('%s: not recording move - %s does not exist\n') %
319 319 (relsrc, reltarget))
320 320 else:
321 321 ui.warn(_('%s: not recording copy - %s does not exist\n') %
322 322 (relsrc, reltarget))
323 323 return
324 324 elif not dryrun:
325 325 try:
326 326 if exists:
327 327 os.unlink(target)
328 328 targetdir = os.path.dirname(target) or '.'
329 329 if not os.path.isdir(targetdir):
330 330 os.makedirs(targetdir)
331 331 if samefile:
332 332 tmp = target + "~hgrename"
333 333 os.rename(src, tmp)
334 334 os.rename(tmp, target)
335 335 else:
336 336 util.copyfile(src, target)
337 337 srcexists = True
338 338 except IOError, inst:
339 339 if inst.errno == errno.ENOENT:
340 340 ui.warn(_('%s: deleted in working copy\n') % relsrc)
341 341 srcexists = False
342 342 else:
343 343 ui.warn(_('%s: cannot copy - %s\n') %
344 344 (relsrc, inst.strerror))
345 345 return True # report a failure
346 346
347 347 if ui.verbose or not exact:
348 348 if rename:
349 349 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
350 350 else:
351 351 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
352 352
353 353 targets[abstarget] = abssrc
354 354
355 355 # fix up dirstate
356 356 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
357 357 dryrun=dryrun, cwd=cwd)
358 358 if rename and not dryrun:
359 359 if not after and srcexists and not samefile:
360 360 util.unlinkpath(repo.wjoin(abssrc))
361 361 wctx.forget([abssrc])
362 362
363 363 # pat: ossep
364 364 # dest ossep
365 365 # srcs: list of (hgsep, hgsep, ossep, bool)
366 366 # return: function that takes hgsep and returns ossep
367 367 def targetpathfn(pat, dest, srcs):
368 368 if os.path.isdir(pat):
369 369 abspfx = scmutil.canonpath(repo.root, cwd, pat)
370 370 abspfx = util.localpath(abspfx)
371 371 if destdirexists:
372 372 striplen = len(os.path.split(abspfx)[0])
373 373 else:
374 374 striplen = len(abspfx)
375 375 if striplen:
376 376 striplen += len(os.sep)
377 377 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
378 378 elif destdirexists:
379 379 res = lambda p: os.path.join(dest,
380 380 os.path.basename(util.localpath(p)))
381 381 else:
382 382 res = lambda p: dest
383 383 return res
384 384
385 385 # pat: ossep
386 386 # dest ossep
387 387 # srcs: list of (hgsep, hgsep, ossep, bool)
388 388 # return: function that takes hgsep and returns ossep
389 389 def targetpathafterfn(pat, dest, srcs):
390 390 if matchmod.patkind(pat):
391 391 # a mercurial pattern
392 392 res = lambda p: os.path.join(dest,
393 393 os.path.basename(util.localpath(p)))
394 394 else:
395 395 abspfx = scmutil.canonpath(repo.root, cwd, pat)
396 396 if len(abspfx) < len(srcs[0][0]):
397 397 # A directory. Either the target path contains the last
398 398 # component of the source path or it does not.
399 399 def evalpath(striplen):
400 400 score = 0
401 401 for s in srcs:
402 402 t = os.path.join(dest, util.localpath(s[0])[striplen:])
403 403 if os.path.lexists(t):
404 404 score += 1
405 405 return score
406 406
407 407 abspfx = util.localpath(abspfx)
408 408 striplen = len(abspfx)
409 409 if striplen:
410 410 striplen += len(os.sep)
411 411 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
412 412 score = evalpath(striplen)
413 413 striplen1 = len(os.path.split(abspfx)[0])
414 414 if striplen1:
415 415 striplen1 += len(os.sep)
416 416 if evalpath(striplen1) > score:
417 417 striplen = striplen1
418 418 res = lambda p: os.path.join(dest,
419 419 util.localpath(p)[striplen:])
420 420 else:
421 421 # a file
422 422 if destdirexists:
423 423 res = lambda p: os.path.join(dest,
424 424 os.path.basename(util.localpath(p)))
425 425 else:
426 426 res = lambda p: dest
427 427 return res
428 428
429 429
430 430 pats = scmutil.expandpats(pats)
431 431 if not pats:
432 432 raise util.Abort(_('no source or destination specified'))
433 433 if len(pats) == 1:
434 434 raise util.Abort(_('no destination specified'))
435 435 dest = pats.pop()
436 436 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
437 437 if not destdirexists:
438 438 if len(pats) > 1 or matchmod.patkind(pats[0]):
439 439 raise util.Abort(_('with multiple sources, destination must be an '
440 440 'existing directory'))
441 441 if util.endswithsep(dest):
442 442 raise util.Abort(_('destination %s is not a directory') % dest)
443 443
444 444 tfn = targetpathfn
445 445 if after:
446 446 tfn = targetpathafterfn
447 447 copylist = []
448 448 for pat in pats:
449 449 srcs = walkpat(pat)
450 450 if not srcs:
451 451 continue
452 452 copylist.append((tfn(pat, dest, srcs), srcs))
453 453 if not copylist:
454 454 raise util.Abort(_('no files to copy'))
455 455
456 456 errors = 0
457 457 for targetpath, srcs in copylist:
458 458 for abssrc, relsrc, exact in srcs:
459 459 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
460 460 errors += 1
461 461
462 462 if errors:
463 463 ui.warn(_('(consider using --after)\n'))
464 464
465 465 return errors != 0
466 466
467 467 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
468 468 runargs=None, appendpid=False):
469 469 '''Run a command as a service.'''
470 470
471 471 if opts['daemon'] and not opts['daemon_pipefds']:
472 472 # Signal child process startup with file removal
473 473 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
474 474 os.close(lockfd)
475 475 try:
476 476 if not runargs:
477 477 runargs = util.hgcmd() + sys.argv[1:]
478 478 runargs.append('--daemon-pipefds=%s' % lockpath)
479 479 # Don't pass --cwd to the child process, because we've already
480 480 # changed directory.
481 481 for i in xrange(1, len(runargs)):
482 482 if runargs[i].startswith('--cwd='):
483 483 del runargs[i]
484 484 break
485 485 elif runargs[i].startswith('--cwd'):
486 486 del runargs[i:i + 2]
487 487 break
488 488 def condfn():
489 489 return not os.path.exists(lockpath)
490 490 pid = util.rundetached(runargs, condfn)
491 491 if pid < 0:
492 492 raise util.Abort(_('child process failed to start'))
493 493 finally:
494 494 try:
495 495 os.unlink(lockpath)
496 496 except OSError, e:
497 497 if e.errno != errno.ENOENT:
498 498 raise
499 499 if parentfn:
500 500 return parentfn(pid)
501 501 else:
502 502 return
503 503
504 504 if initfn:
505 505 initfn()
506 506
507 507 if opts['pid_file']:
508 508 mode = appendpid and 'a' or 'w'
509 509 fp = open(opts['pid_file'], mode)
510 510 fp.write(str(os.getpid()) + '\n')
511 511 fp.close()
512 512
513 513 if opts['daemon_pipefds']:
514 514 lockpath = opts['daemon_pipefds']
515 515 try:
516 516 os.setsid()
517 517 except AttributeError:
518 518 pass
519 519 os.unlink(lockpath)
520 520 util.hidewindow()
521 521 sys.stdout.flush()
522 522 sys.stderr.flush()
523 523
524 524 nullfd = os.open(os.devnull, os.O_RDWR)
525 525 logfilefd = nullfd
526 526 if logfile:
527 527 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
528 528 os.dup2(nullfd, 0)
529 529 os.dup2(logfilefd, 1)
530 530 os.dup2(logfilefd, 2)
531 531 if nullfd not in (0, 1, 2):
532 532 os.close(nullfd)
533 533 if logfile and logfilefd not in (0, 1, 2):
534 534 os.close(logfilefd)
535 535
536 536 if runfn:
537 537 return runfn()
538 538
539 539 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
540 540 opts=None):
541 541 '''export changesets as hg patches.'''
542 542
543 543 total = len(revs)
544 544 revwidth = max([len(str(rev)) for rev in revs])
545 545 filemode = {}
546 546
547 547 def single(rev, seqno, fp):
548 548 ctx = repo[rev]
549 549 node = ctx.node()
550 550 parents = [p.node() for p in ctx.parents() if p]
551 551 branch = ctx.branch()
552 552 if switch_parent:
553 553 parents.reverse()
554 554 prev = (parents and parents[0]) or nullid
555 555
556 556 shouldclose = False
557 557 if not fp and len(template) > 0:
558 558 desc_lines = ctx.description().rstrip().split('\n')
559 559 desc = desc_lines[0] #Commit always has a first line.
560 560 fp = makefileobj(repo, template, node, desc=desc, total=total,
561 561 seqno=seqno, revwidth=revwidth, mode='wb',
562 562 modemap=filemode)
563 563 if fp != template:
564 564 shouldclose = True
565 565 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
566 566 repo.ui.note("%s\n" % fp.name)
567 567
568 568 if not fp:
569 569 write = repo.ui.write
570 570 else:
571 571 def write(s, **kw):
572 572 fp.write(s)
573 573
574 574
575 575 write("# HG changeset patch\n")
576 576 write("# User %s\n" % ctx.user())
577 577 write("# Date %d %d\n" % ctx.date())
578 578 write("# %s\n" % util.datestr(ctx.date()))
579 579 if branch and branch != 'default':
580 580 write("# Branch %s\n" % branch)
581 581 write("# Node ID %s\n" % hex(node))
582 582 write("# Parent %s\n" % hex(prev))
583 583 if len(parents) > 1:
584 584 write("# Parent %s\n" % hex(parents[1]))
585 585 write(ctx.description().rstrip())
586 586 write("\n\n")
587 587
588 588 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
589 589 write(chunk, label=label)
590 590
591 591 if shouldclose:
592 592 fp.close()
593 593
594 594 for seqno, rev in enumerate(revs):
595 595 single(rev, seqno + 1, fp)
596 596
597 597 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
598 598 changes=None, stat=False, fp=None, prefix='',
599 599 listsubrepos=False):
600 600 '''show diff or diffstat.'''
601 601 if fp is None:
602 602 write = ui.write
603 603 else:
604 604 def write(s, **kw):
605 605 fp.write(s)
606 606
607 607 if stat:
608 608 diffopts = diffopts.copy(context=0)
609 609 width = 80
610 610 if not ui.plain():
611 611 width = ui.termwidth()
612 612 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
613 613 prefix=prefix)
614 614 for chunk, label in patch.diffstatui(util.iterlines(chunks),
615 615 width=width,
616 616 git=diffopts.git):
617 617 write(chunk, label=label)
618 618 else:
619 619 for chunk, label in patch.diffui(repo, node1, node2, match,
620 620 changes, diffopts, prefix=prefix):
621 621 write(chunk, label=label)
622 622
623 623 if listsubrepos:
624 624 ctx1 = repo[node1]
625 625 ctx2 = repo[node2]
626 626 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
627 627 tempnode2 = node2
628 628 try:
629 629 if node2 is not None:
630 630 tempnode2 = ctx2.substate[subpath][1]
631 631 except KeyError:
632 632 # A subrepo that existed in node1 was deleted between node1 and
633 633 # node2 (inclusive). Thus, ctx2's substate won't contain that
634 634 # subpath. The best we can do is to ignore it.
635 635 tempnode2 = None
636 636 submatch = matchmod.narrowmatcher(subpath, match)
637 637 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
638 638 stat=stat, fp=fp, prefix=prefix)
639 639
640 640 class changeset_printer(object):
641 641 '''show changeset information when templating not requested.'''
642 642
643 643 def __init__(self, ui, repo, patch, diffopts, buffered):
644 644 self.ui = ui
645 645 self.repo = repo
646 646 self.buffered = buffered
647 647 self.patch = patch
648 648 self.diffopts = diffopts
649 649 self.header = {}
650 650 self.hunk = {}
651 651 self.lastheader = None
652 652 self.footer = None
653 653
654 654 def flush(self, rev):
655 655 if rev in self.header:
656 656 h = self.header[rev]
657 657 if h != self.lastheader:
658 658 self.lastheader = h
659 659 self.ui.write(h)
660 660 del self.header[rev]
661 661 if rev in self.hunk:
662 662 self.ui.write(self.hunk[rev])
663 663 del self.hunk[rev]
664 664 return 1
665 665 return 0
666 666
667 667 def close(self):
668 668 if self.footer:
669 669 self.ui.write(self.footer)
670 670
671 671 def show(self, ctx, copies=None, matchfn=None, **props):
672 672 if self.buffered:
673 673 self.ui.pushbuffer()
674 674 self._show(ctx, copies, matchfn, props)
675 675 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
676 676 else:
677 677 self._show(ctx, copies, matchfn, props)
678 678
679 679 def _show(self, ctx, copies, matchfn, props):
680 680 '''show a single changeset or file revision'''
681 681 changenode = ctx.node()
682 682 rev = ctx.rev()
683 683
684 684 if self.ui.quiet:
685 685 self.ui.write("%d:%s\n" % (rev, short(changenode)),
686 686 label='log.node')
687 687 return
688 688
689 689 log = self.repo.changelog
690 690 date = util.datestr(ctx.date())
691 691
692 692 hexfunc = self.ui.debugflag and hex or short
693 693
694 694 parents = [(p, hexfunc(log.node(p)))
695 695 for p in self._meaningful_parentrevs(log, rev)]
696 696
697 697 # i18n: column positioning for "hg log"
698 698 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
699 699 label='log.changeset changeset.%s' % ctx.phasestr())
700 700
701 701 branch = ctx.branch()
702 702 # don't show the default branch name
703 703 if branch != 'default':
704 704 # i18n: column positioning for "hg log"
705 705 self.ui.write(_("branch: %s\n") % branch,
706 706 label='log.branch')
707 707 for bookmark in self.repo.nodebookmarks(changenode):
708 708 # i18n: column positioning for "hg log"
709 709 self.ui.write(_("bookmark: %s\n") % bookmark,
710 710 label='log.bookmark')
711 711 for tag in self.repo.nodetags(changenode):
712 712 # i18n: column positioning for "hg log"
713 713 self.ui.write(_("tag: %s\n") % tag,
714 714 label='log.tag')
715 715 if self.ui.debugflag and ctx.phase():
716 716 # i18n: column positioning for "hg log"
717 717 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
718 718 label='log.phase')
719 719 for parent in parents:
720 720 # i18n: column positioning for "hg log"
721 721 self.ui.write(_("parent: %d:%s\n") % parent,
722 722 label='log.parent changeset.%s' % ctx.phasestr())
723 723
724 724 if self.ui.debugflag:
725 725 mnode = ctx.manifestnode()
726 726 # i18n: column positioning for "hg log"
727 727 self.ui.write(_("manifest: %d:%s\n") %
728 728 (self.repo.manifest.rev(mnode), hex(mnode)),
729 729 label='ui.debug log.manifest')
730 730 # i18n: column positioning for "hg log"
731 731 self.ui.write(_("user: %s\n") % ctx.user(),
732 732 label='log.user')
733 733 # i18n: column positioning for "hg log"
734 734 self.ui.write(_("date: %s\n") % date,
735 735 label='log.date')
736 736
737 737 if self.ui.debugflag:
738 738 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
739 739 for key, value in zip([# i18n: column positioning for "hg log"
740 740 _("files:"),
741 741 # i18n: column positioning for "hg log"
742 742 _("files+:"),
743 743 # i18n: column positioning for "hg log"
744 744 _("files-:")], files):
745 745 if value:
746 746 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
747 747 label='ui.debug log.files')
748 748 elif ctx.files() and self.ui.verbose:
749 749 # i18n: column positioning for "hg log"
750 750 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
751 751 label='ui.note log.files')
752 752 if copies and self.ui.verbose:
753 753 copies = ['%s (%s)' % c for c in copies]
754 754 # i18n: column positioning for "hg log"
755 755 self.ui.write(_("copies: %s\n") % ' '.join(copies),
756 756 label='ui.note log.copies')
757 757
758 758 extra = ctx.extra()
759 759 if extra and self.ui.debugflag:
760 760 for key, value in sorted(extra.items()):
761 761 # i18n: column positioning for "hg log"
762 762 self.ui.write(_("extra: %s=%s\n")
763 763 % (key, value.encode('string_escape')),
764 764 label='ui.debug log.extra')
765 765
766 766 description = ctx.description().strip()
767 767 if description:
768 768 if self.ui.verbose:
769 769 self.ui.write(_("description:\n"),
770 770 label='ui.note log.description')
771 771 self.ui.write(description,
772 772 label='ui.note log.description')
773 773 self.ui.write("\n\n")
774 774 else:
775 775 # i18n: column positioning for "hg log"
776 776 self.ui.write(_("summary: %s\n") %
777 777 description.splitlines()[0],
778 778 label='log.summary')
779 779 self.ui.write("\n")
780 780
781 781 self.showpatch(changenode, matchfn)
782 782
783 783 def showpatch(self, node, matchfn):
784 784 if not matchfn:
785 785 matchfn = self.patch
786 786 if matchfn:
787 787 stat = self.diffopts.get('stat')
788 788 diff = self.diffopts.get('patch')
789 789 diffopts = patch.diffopts(self.ui, self.diffopts)
790 790 prev = self.repo.changelog.parents(node)[0]
791 791 if stat:
792 792 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
793 793 match=matchfn, stat=True)
794 794 if diff:
795 795 if stat:
796 796 self.ui.write("\n")
797 797 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
798 798 match=matchfn, stat=False)
799 799 self.ui.write("\n")
800 800
801 801 def _meaningful_parentrevs(self, log, rev):
802 802 """Return list of meaningful (or all if debug) parentrevs for rev.
803 803
804 804 For merges (two non-nullrev revisions) both parents are meaningful.
805 805 Otherwise the first parent revision is considered meaningful if it
806 806 is not the preceding revision.
807 807 """
808 808 parents = log.parentrevs(rev)
809 809 if not self.ui.debugflag and parents[1] == nullrev:
810 810 if parents[0] >= rev - 1:
811 811 parents = []
812 812 else:
813 813 parents = [parents[0]]
814 814 return parents
815 815
816 816
817 817 class changeset_templater(changeset_printer):
818 818 '''format changeset information.'''
819 819
820 820 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
821 821 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
822 822 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
823 823 defaulttempl = {
824 824 'parent': '{rev}:{node|formatnode} ',
825 825 'manifest': '{rev}:{node|formatnode}',
826 826 'file_copy': '{name} ({source})',
827 827 'extra': '{key}={value|stringescape}'
828 828 }
829 829 # filecopy is preserved for compatibility reasons
830 830 defaulttempl['filecopy'] = defaulttempl['file_copy']
831 831 self.t = templater.templater(mapfile, {'formatnode': formatnode},
832 832 cache=defaulttempl)
833 833 self.cache = {}
834 834
835 835 def use_template(self, t):
836 836 '''set template string to use'''
837 837 self.t.cache['changeset'] = t
838 838
839 839 def _meaningful_parentrevs(self, ctx):
840 840 """Return list of meaningful (or all if debug) parentrevs for rev.
841 841 """
842 842 parents = ctx.parents()
843 843 if len(parents) > 1:
844 844 return parents
845 845 if self.ui.debugflag:
846 846 return [parents[0], self.repo['null']]
847 847 if parents[0].rev() >= ctx.rev() - 1:
848 848 return []
849 849 return parents
850 850
851 851 def _show(self, ctx, copies, matchfn, props):
852 852 '''show a single changeset or file revision'''
853 853
854 854 showlist = templatekw.showlist
855 855
856 856 # showparents() behaviour depends on ui trace level which
857 857 # causes unexpected behaviours at templating level and makes
858 858 # it harder to extract it in a standalone function. Its
859 859 # behaviour cannot be changed so leave it here for now.
860 860 def showparents(**args):
861 861 ctx = args['ctx']
862 862 parents = [[('rev', p.rev()), ('node', p.hex())]
863 863 for p in self._meaningful_parentrevs(ctx)]
864 864 return showlist('parent', parents, **args)
865 865
866 866 props = props.copy()
867 867 props.update(templatekw.keywords)
868 868 props['parents'] = showparents
869 869 props['templ'] = self.t
870 870 props['ctx'] = ctx
871 871 props['repo'] = self.repo
872 872 props['revcache'] = {'copies': copies}
873 873 props['cache'] = self.cache
874 874
875 875 # find correct templates for current mode
876 876
877 877 tmplmodes = [
878 878 (True, None),
879 879 (self.ui.verbose, 'verbose'),
880 880 (self.ui.quiet, 'quiet'),
881 881 (self.ui.debugflag, 'debug'),
882 882 ]
883 883
884 884 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
885 885 for mode, postfix in tmplmodes:
886 886 for type in types:
887 887 cur = postfix and ('%s_%s' % (type, postfix)) or type
888 888 if mode and cur in self.t:
889 889 types[type] = cur
890 890
891 891 try:
892 892
893 893 # write header
894 894 if types['header']:
895 895 h = templater.stringify(self.t(types['header'], **props))
896 896 if self.buffered:
897 897 self.header[ctx.rev()] = h
898 898 else:
899 899 if self.lastheader != h:
900 900 self.lastheader = h
901 901 self.ui.write(h)
902 902
903 903 # write changeset metadata, then patch if requested
904 904 key = types['changeset']
905 905 self.ui.write(templater.stringify(self.t(key, **props)))
906 906 self.showpatch(ctx.node(), matchfn)
907 907
908 908 if types['footer']:
909 909 if not self.footer:
910 910 self.footer = templater.stringify(self.t(types['footer'],
911 911 **props))
912 912
913 913 except KeyError, inst:
914 914 msg = _("%s: no key named '%s'")
915 915 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
916 916 except SyntaxError, inst:
917 917 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
918 918
919 919 def show_changeset(ui, repo, opts, buffered=False):
920 920 """show one changeset using template or regular display.
921 921
922 922 Display format will be the first non-empty hit of:
923 923 1. option 'template'
924 924 2. option 'style'
925 925 3. [ui] setting 'logtemplate'
926 926 4. [ui] setting 'style'
927 927 If all of these values are either the unset or the empty string,
928 928 regular display via changeset_printer() is done.
929 929 """
930 930 # options
931 931 patch = False
932 932 if opts.get('patch') or opts.get('stat'):
933 933 patch = scmutil.matchall(repo)
934 934
935 935 tmpl = opts.get('template')
936 936 style = None
937 937 if tmpl:
938 938 tmpl = templater.parsestring(tmpl, quoted=False)
939 939 else:
940 940 style = opts.get('style')
941 941
942 942 # ui settings
943 943 if not (tmpl or style):
944 944 tmpl = ui.config('ui', 'logtemplate')
945 945 if tmpl:
946 946 try:
947 947 tmpl = templater.parsestring(tmpl)
948 948 except SyntaxError:
949 949 tmpl = templater.parsestring(tmpl, quoted=False)
950 950 else:
951 951 style = util.expandpath(ui.config('ui', 'style', ''))
952 952
953 953 if not (tmpl or style):
954 954 return changeset_printer(ui, repo, patch, opts, buffered)
955 955
956 956 mapfile = None
957 957 if style and not tmpl:
958 958 mapfile = style
959 959 if not os.path.split(mapfile)[0]:
960 960 mapname = (templater.templatepath('map-cmdline.' + mapfile)
961 961 or templater.templatepath(mapfile))
962 962 if mapname:
963 963 mapfile = mapname
964 964
965 965 try:
966 966 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
967 967 except SyntaxError, inst:
968 968 raise util.Abort(inst.args[0])
969 969 if tmpl:
970 970 t.use_template(tmpl)
971 971 return t
972 972
973 973 def finddate(ui, repo, date):
974 974 """Find the tipmost changeset that matches the given date spec"""
975 975
976 976 df = util.matchdate(date)
977 977 m = scmutil.matchall(repo)
978 978 results = {}
979 979
980 980 def prep(ctx, fns):
981 981 d = ctx.date()
982 982 if df(d[0]):
983 983 results[ctx.rev()] = d
984 984
985 985 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
986 986 rev = ctx.rev()
987 987 if rev in results:
988 988 ui.status(_("found revision %s from %s\n") %
989 989 (rev, util.datestr(results[rev])))
990 990 return str(rev)
991 991
992 992 raise util.Abort(_("revision matching date not found"))
993 993
994 994 def increasingwindows(start, end, windowsize=8, sizelimit=512):
995 995 if start < end:
996 996 while start < end:
997 997 yield start, min(windowsize, end - start)
998 998 start += windowsize
999 999 if windowsize < sizelimit:
1000 1000 windowsize *= 2
1001 1001 else:
1002 1002 while start > end:
1003 1003 yield start, min(windowsize, start - end - 1)
1004 1004 start -= windowsize
1005 1005 if windowsize < sizelimit:
1006 1006 windowsize *= 2
1007 1007
1008 1008 def walkchangerevs(repo, match, opts, prepare):
1009 1009 '''Iterate over files and the revs in which they changed.
1010 1010
1011 1011 Callers most commonly need to iterate backwards over the history
1012 1012 in which they are interested. Doing so has awful (quadratic-looking)
1013 1013 performance, so we use iterators in a "windowed" way.
1014 1014
1015 1015 We walk a window of revisions in the desired order. Within the
1016 1016 window, we first walk forwards to gather data, then in the desired
1017 1017 order (usually backwards) to display it.
1018 1018
1019 1019 This function returns an iterator yielding contexts. Before
1020 1020 yielding each context, the iterator will first call the prepare
1021 1021 function on each context in the window in forward order.'''
1022 1022
1023 1023 follow = opts.get('follow') or opts.get('follow_first')
1024 1024
1025 1025 if opts.get('rev'):
1026 1026 revs = scmutil.revrange(repo, opts.get('rev'))
1027 1027 elif follow:
1028 1028 revs = repo.revs('reverse(:.)')
1029 1029 else:
1030 1030 revs = list(repo)
1031 1031 revs.reverse()
1032 1032 if not revs:
1033 1033 return []
1034 1034 wanted = set()
1035 1035 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1036 1036 fncache = {}
1037 1037 change = repo.changectx
1038 1038
1039 1039 # First step is to fill wanted, the set of revisions that we want to yield.
1040 1040 # When it does not induce extra cost, we also fill fncache for revisions in
1041 1041 # wanted: a cache of filenames that were changed (ctx.files()) and that
1042 1042 # match the file filtering conditions.
1043 1043
1044 1044 if not slowpath and not match.files():
1045 1045 # No files, no patterns. Display all revs.
1046 1046 wanted = set(revs)
1047 1047 copies = []
1048 1048
1049 1049 if not slowpath and match.files():
1050 1050 # We only have to read through the filelog to find wanted revisions
1051 1051
1052 1052 minrev, maxrev = min(revs), max(revs)
1053 1053 def filerevgen(filelog, last):
1054 1054 """
1055 1055 Only files, no patterns. Check the history of each file.
1056 1056
1057 1057 Examines filelog entries within minrev, maxrev linkrev range
1058 1058 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1059 1059 tuples in backwards order
1060 1060 """
1061 1061 cl_count = len(repo)
1062 1062 revs = []
1063 1063 for j in xrange(0, last + 1):
1064 1064 linkrev = filelog.linkrev(j)
1065 1065 if linkrev < minrev:
1066 1066 continue
1067 1067 # only yield rev for which we have the changelog, it can
1068 1068 # happen while doing "hg log" during a pull or commit
1069 1069 if linkrev >= cl_count:
1070 1070 break
1071 1071
1072 1072 parentlinkrevs = []
1073 1073 for p in filelog.parentrevs(j):
1074 1074 if p != nullrev:
1075 1075 parentlinkrevs.append(filelog.linkrev(p))
1076 1076 n = filelog.node(j)
1077 1077 revs.append((linkrev, parentlinkrevs,
1078 1078 follow and filelog.renamed(n)))
1079 1079
1080 1080 return reversed(revs)
1081 1081 def iterfiles():
1082 1082 pctx = repo['.']
1083 1083 for filename in match.files():
1084 1084 if follow:
1085 1085 if filename not in pctx:
1086 1086 raise util.Abort(_('cannot follow file not in parent '
1087 1087 'revision: "%s"') % filename)
1088 1088 yield filename, pctx[filename].filenode()
1089 1089 else:
1090 1090 yield filename, None
1091 1091 for filename_node in copies:
1092 1092 yield filename_node
1093 1093 for file_, node in iterfiles():
1094 1094 filelog = repo.file(file_)
1095 1095 if not len(filelog):
1096 1096 if node is None:
1097 1097 # A zero count may be a directory or deleted file, so
1098 1098 # try to find matching entries on the slow path.
1099 1099 if follow:
1100 1100 raise util.Abort(
1101 1101 _('cannot follow nonexistent file: "%s"') % file_)
1102 1102 slowpath = True
1103 1103 break
1104 1104 else:
1105 1105 continue
1106 1106
1107 1107 if node is None:
1108 1108 last = len(filelog) - 1
1109 1109 else:
1110 1110 last = filelog.rev(node)
1111 1111
1112 1112
1113 1113 # keep track of all ancestors of the file
1114 1114 ancestors = set([filelog.linkrev(last)])
1115 1115
1116 1116 # iterate from latest to oldest revision
1117 1117 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1118 1118 if not follow:
1119 1119 if rev > maxrev:
1120 1120 continue
1121 1121 else:
1122 1122 # Note that last might not be the first interesting
1123 1123 # rev to us:
1124 1124 # if the file has been changed after maxrev, we'll
1125 1125 # have linkrev(last) > maxrev, and we still need
1126 1126 # to explore the file graph
1127 1127 if rev not in ancestors:
1128 1128 continue
1129 1129 # XXX insert 1327 fix here
1130 1130 if flparentlinkrevs:
1131 1131 ancestors.update(flparentlinkrevs)
1132 1132
1133 1133 fncache.setdefault(rev, []).append(file_)
1134 1134 wanted.add(rev)
1135 1135 if copied:
1136 1136 copies.append(copied)
1137 1137
1138 1138 # We decided to fall back to the slowpath because at least one
1139 1139 # of the paths was not a file. Check to see if at least one of them
1140 1140 # existed in history, otherwise simply return
1141 1141 if slowpath:
1142 1142 for path in match.files():
1143 1143 if path == '.' or path in repo.store:
1144 1144 break
1145 1145 else:
1146 1146 return []
1147 1147
1148 1148 if slowpath:
1149 1149 # We have to read the changelog to match filenames against
1150 1150 # changed files
1151 1151
1152 1152 if follow:
1153 1153 raise util.Abort(_('can only follow copies/renames for explicit '
1154 1154 'filenames'))
1155 1155
1156 1156 # The slow path checks files modified in every changeset.
1157 1157 for i in sorted(revs):
1158 1158 ctx = change(i)
1159 1159 matches = filter(match, ctx.files())
1160 1160 if matches:
1161 1161 fncache[i] = matches
1162 1162 wanted.add(i)
1163 1163
1164 1164 class followfilter(object):
1165 1165 def __init__(self, onlyfirst=False):
1166 1166 self.startrev = nullrev
1167 1167 self.roots = set()
1168 1168 self.onlyfirst = onlyfirst
1169 1169
1170 1170 def match(self, rev):
1171 1171 def realparents(rev):
1172 1172 if self.onlyfirst:
1173 1173 return repo.changelog.parentrevs(rev)[0:1]
1174 1174 else:
1175 1175 return filter(lambda x: x != nullrev,
1176 1176 repo.changelog.parentrevs(rev))
1177 1177
1178 1178 if self.startrev == nullrev:
1179 1179 self.startrev = rev
1180 1180 return True
1181 1181
1182 1182 if rev > self.startrev:
1183 1183 # forward: all descendants
1184 1184 if not self.roots:
1185 1185 self.roots.add(self.startrev)
1186 1186 for parent in realparents(rev):
1187 1187 if parent in self.roots:
1188 1188 self.roots.add(rev)
1189 1189 return True
1190 1190 else:
1191 1191 # backwards: all parents
1192 1192 if not self.roots:
1193 1193 self.roots.update(realparents(self.startrev))
1194 1194 if rev in self.roots:
1195 1195 self.roots.remove(rev)
1196 1196 self.roots.update(realparents(rev))
1197 1197 return True
1198 1198
1199 1199 return False
1200 1200
1201 1201 # it might be worthwhile to do this in the iterator if the rev range
1202 1202 # is descending and the prune args are all within that range
1203 1203 for rev in opts.get('prune', ()):
1204 1204 rev = repo[rev].rev()
1205 1205 ff = followfilter()
1206 1206 stop = min(revs[0], revs[-1])
1207 1207 for x in xrange(rev, stop - 1, -1):
1208 1208 if ff.match(x):
1209 1209 wanted.discard(x)
1210 1210
1211 1211 # Choose a small initial window if we will probably only visit a
1212 1212 # few commits.
1213 1213 limit = loglimit(opts)
1214 1214 windowsize = 8
1215 1215 if limit:
1216 1216 windowsize = min(limit, windowsize)
1217 1217
1218 1218 # Now that wanted is correctly initialized, we can iterate over the
1219 1219 # revision range, yielding only revisions in wanted.
1220 1220 def iterate():
1221 1221 if follow and not match.files():
1222 1222 ff = followfilter(onlyfirst=opts.get('follow_first'))
1223 1223 def want(rev):
1224 1224 return ff.match(rev) and rev in wanted
1225 1225 else:
1226 1226 def want(rev):
1227 1227 return rev in wanted
1228 1228
1229 1229 for i, window in increasingwindows(0, len(revs), windowsize):
1230 1230 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1231 1231 for rev in sorted(nrevs):
1232 1232 fns = fncache.get(rev)
1233 1233 ctx = change(rev)
1234 1234 if not fns:
1235 1235 def fns_generator():
1236 1236 for f in ctx.files():
1237 1237 if match(f):
1238 1238 yield f
1239 1239 fns = fns_generator()
1240 1240 prepare(ctx, fns)
1241 1241 for rev in nrevs:
1242 1242 yield change(rev)
1243 1243 return iterate()
1244 1244
1245 1245 def _makegraphfilematcher(repo, pats, followfirst):
1246 1246 # When displaying a revision with --patch --follow FILE, we have
1247 1247 # to know which file of the revision must be diffed. With
1248 1248 # --follow, we want the names of the ancestors of FILE in the
1249 1249 # revision, stored in "fcache". "fcache" is populated by
1250 1250 # reproducing the graph traversal already done by --follow revset
1251 1251 # and relating linkrevs to file names (which is not "correct" but
1252 1252 # good enough).
1253 1253 fcache = {}
1254 1254 fcacheready = [False]
1255 1255 pctx = repo['.']
1256 1256 wctx = repo[None]
1257 1257
1258 1258 def populate():
1259 1259 for fn in pats:
1260 1260 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1261 1261 for c in i:
1262 1262 fcache.setdefault(c.linkrev(), set()).add(c.path())
1263 1263
1264 1264 def filematcher(rev):
1265 1265 if not fcacheready[0]:
1266 1266 # Lazy initialization
1267 1267 fcacheready[0] = True
1268 1268 populate()
1269 1269 return scmutil.match(wctx, fcache.get(rev, []), default='path')
1270 1270
1271 1271 return filematcher
1272 1272
1273 1273 def _makegraphlogrevset(repo, pats, opts, revs):
1274 1274 """Return (expr, filematcher) where expr is a revset string built
1275 1275 from log options and file patterns or None. If --stat or --patch
1276 1276 are not passed filematcher is None. Otherwise it is a callable
1277 1277 taking a revision number and returning a match objects filtering
1278 1278 the files to be detailed when displaying the revision.
1279 1279 """
1280 1280 opt2revset = {
1281 1281 'no_merges': ('not merge()', None),
1282 1282 'only_merges': ('merge()', None),
1283 1283 '_ancestors': ('ancestors(%(val)s)', None),
1284 1284 '_fancestors': ('_firstancestors(%(val)s)', None),
1285 1285 '_descendants': ('descendants(%(val)s)', None),
1286 1286 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1287 1287 '_matchfiles': ('_matchfiles(%(val)s)', None),
1288 1288 'date': ('date(%(val)r)', None),
1289 1289 'branch': ('branch(%(val)r)', ' or '),
1290 1290 '_patslog': ('filelog(%(val)r)', ' or '),
1291 1291 '_patsfollow': ('follow(%(val)r)', ' or '),
1292 1292 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1293 1293 'keyword': ('keyword(%(val)r)', ' or '),
1294 1294 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1295 1295 'user': ('user(%(val)r)', ' or '),
1296 1296 }
1297 1297
1298 1298 opts = dict(opts)
1299 1299 # follow or not follow?
1300 1300 follow = opts.get('follow') or opts.get('follow_first')
1301 1301 followfirst = opts.get('follow_first') and 1 or 0
1302 1302 # --follow with FILE behaviour depends on revs...
1303 1303 startrev = revs[0]
1304 1304 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
1305 1305
1306 1306 # branch and only_branch are really aliases and must be handled at
1307 1307 # the same time
1308 1308 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1309 1309 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1310 1310 # pats/include/exclude are passed to match.match() directly in
1311 1311 # _matchfiles() revset but walkchangerevs() builds its matcher with
1312 1312 # scmutil.match(). The difference is input pats are globbed on
1313 1313 # platforms without shell expansion (windows).
1314 1314 pctx = repo[None]
1315 1315 match, pats = scmutil.matchandpats(pctx, pats, opts)
1316 1316 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1317 1317 if not slowpath:
1318 1318 for f in match.files():
1319 1319 if follow and f not in pctx:
1320 1320 raise util.Abort(_('cannot follow file not in parent '
1321 1321 'revision: "%s"') % f)
1322 1322 filelog = repo.file(f)
1323 1323 if not len(filelog):
1324 1324 # A zero count may be a directory or deleted file, so
1325 1325 # try to find matching entries on the slow path.
1326 1326 if follow:
1327 1327 raise util.Abort(
1328 1328 _('cannot follow nonexistent file: "%s"') % f)
1329 1329 slowpath = True
1330 1330
1331 1331 # We decided to fall back to the slowpath because at least one
1332 1332 # of the paths was not a file. Check to see if at least one of them
1333 1333 # existed in history - in that case, we'll continue down the
1334 1334 # slowpath; otherwise, we can turn off the slowpath
1335 1335 if slowpath:
1336 1336 for path in match.files():
1337 1337 if path == '.' or path in repo.store:
1338 1338 break
1339 1339 else:
1340 1340 slowpath = False
1341 1341
1342 1342 if slowpath:
1343 1343 # See walkchangerevs() slow path.
1344 1344 #
1345 1345 if follow:
1346 1346 raise util.Abort(_('can only follow copies/renames for explicit '
1347 1347 'filenames'))
1348 1348 # pats/include/exclude cannot be represented as separate
1349 1349 # revset expressions as their filtering logic applies at file
1350 1350 # level. For instance "-I a -X a" matches a revision touching
1351 1351 # "a" and "b" while "file(a) and not file(b)" does
1352 1352 # not. Besides, filesets are evaluated against the working
1353 1353 # directory.
1354 1354 matchargs = ['r:', 'd:relpath']
1355 1355 for p in pats:
1356 1356 matchargs.append('p:' + p)
1357 1357 for p in opts.get('include', []):
1358 1358 matchargs.append('i:' + p)
1359 1359 for p in opts.get('exclude', []):
1360 1360 matchargs.append('x:' + p)
1361 1361 matchargs = ','.join(('%r' % p) for p in matchargs)
1362 1362 opts['_matchfiles'] = matchargs
1363 1363 else:
1364 1364 if follow:
1365 1365 fpats = ('_patsfollow', '_patsfollowfirst')
1366 1366 fnopats = (('_ancestors', '_fancestors'),
1367 1367 ('_descendants', '_fdescendants'))
1368 1368 if pats:
1369 1369 # follow() revset interprets its file argument as a
1370 1370 # manifest entry, so use match.files(), not pats.
1371 1371 opts[fpats[followfirst]] = list(match.files())
1372 1372 else:
1373 1373 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1374 1374 else:
1375 1375 opts['_patslog'] = list(pats)
1376 1376
1377 1377 filematcher = None
1378 1378 if opts.get('patch') or opts.get('stat'):
1379 1379 if follow:
1380 1380 filematcher = _makegraphfilematcher(repo, pats, followfirst)
1381 1381 else:
1382 1382 filematcher = lambda rev: match
1383 1383
1384 1384 expr = []
1385 1385 for op, val in opts.iteritems():
1386 1386 if not val:
1387 1387 continue
1388 1388 if op not in opt2revset:
1389 1389 continue
1390 1390 revop, andor = opt2revset[op]
1391 1391 if '%(val)' not in revop:
1392 1392 expr.append(revop)
1393 1393 else:
1394 1394 if not isinstance(val, list):
1395 1395 e = revop % {'val': val}
1396 1396 else:
1397 1397 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1398 1398 expr.append(e)
1399 1399
1400 1400 if expr:
1401 1401 expr = '(' + ' and '.join(expr) + ')'
1402 1402 else:
1403 1403 expr = None
1404 1404 return expr, filematcher
1405 1405
1406 1406 def getgraphlogrevs(repo, pats, opts):
1407 1407 """Return (revs, expr, filematcher) where revs is an iterable of
1408 1408 revision numbers, expr is a revset string built from log options
1409 1409 and file patterns or None, and used to filter 'revs'. If --stat or
1410 1410 --patch are not passed filematcher is None. Otherwise it is a
1411 1411 callable taking a revision number and returning a match objects
1412 1412 filtering the files to be detailed when displaying the revision.
1413 1413 """
1414 1414 if not len(repo):
1415 1415 return [], None, None
1416 1416 limit = loglimit(opts)
1417 1417 # Default --rev value depends on --follow but --follow behaviour
1418 1418 # depends on revisions resolved from --rev...
1419 1419 follow = opts.get('follow') or opts.get('follow_first')
1420 1420 possiblyunsorted = False # whether revs might need sorting
1421 1421 if opts.get('rev'):
1422 1422 revs = scmutil.revrange(repo, opts['rev'])
1423 1423 # Don't sort here because _makegraphlogrevset might depend on the
1424 1424 # order of revs
1425 1425 possiblyunsorted = True
1426 1426 else:
1427 1427 if follow and len(repo) > 0:
1428 1428 revs = repo.revs('reverse(:.)')
1429 1429 else:
1430 1430 revs = list(repo.changelog)
1431 1431 revs.reverse()
1432 1432 if not revs:
1433 1433 return [], None, None
1434 1434 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
1435 1435 if possiblyunsorted:
1436 1436 revs.sort(reverse=True)
1437 1437 if expr:
1438 1438 # Revset matchers often operate faster on revisions in changelog
1439 1439 # order, because most filters deal with the changelog.
1440 1440 revs.reverse()
1441 1441 matcher = revset.match(repo.ui, expr)
1442 1442 # Revset matches can reorder revisions. "A or B" typically returns
1443 1443 # returns the revision matching A then the revision matching B. Sort
1444 1444 # again to fix that.
1445 1445 revs = matcher(repo, revs)
1446 1446 revs.sort(reverse=True)
1447 1447 if limit is not None:
1448 1448 revs = revs[:limit]
1449 1449
1450 1450 return revs, expr, filematcher
1451 1451
1452 1452 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1453 1453 filematcher=None):
1454 1454 seen, state = [], graphmod.asciistate()
1455 1455 for rev, type, ctx, parents in dag:
1456 1456 char = 'o'
1457 1457 if ctx.node() in showparents:
1458 1458 char = '@'
1459 1459 elif ctx.obsolete():
1460 1460 char = 'x'
1461 1461 copies = None
1462 1462 if getrenamed and ctx.rev():
1463 1463 copies = []
1464 1464 for fn in ctx.files():
1465 1465 rename = getrenamed(fn, ctx.rev())
1466 1466 if rename:
1467 1467 copies.append((fn, rename[0]))
1468 1468 revmatchfn = None
1469 1469 if filematcher is not None:
1470 1470 revmatchfn = filematcher(ctx.rev())
1471 1471 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1472 1472 lines = displayer.hunk.pop(rev).split('\n')
1473 1473 if not lines[-1]:
1474 1474 del lines[-1]
1475 1475 displayer.flush(rev)
1476 1476 edges = edgefn(type, char, lines, seen, rev, parents)
1477 1477 for type, char, lines, coldata in edges:
1478 1478 graphmod.ascii(ui, state, type, char, lines, coldata)
1479 1479 displayer.close()
1480 1480
1481 1481 def graphlog(ui, repo, *pats, **opts):
1482 1482 # Parameters are identical to log command ones
1483 1483 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1484 1484 revdag = graphmod.dagwalker(repo, revs)
1485 1485
1486 1486 getrenamed = None
1487 1487 if opts.get('copies'):
1488 1488 endrev = None
1489 1489 if opts.get('rev'):
1490 1490 endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
1491 1491 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1492 1492 displayer = show_changeset(ui, repo, opts, buffered=True)
1493 1493 showparents = [ctx.node() for ctx in repo[None].parents()]
1494 1494 displaygraph(ui, revdag, displayer, showparents,
1495 1495 graphmod.asciiedges, getrenamed, filematcher)
1496 1496
1497 1497 def checkunsupportedgraphflags(pats, opts):
1498 1498 for op in ["newest_first"]:
1499 1499 if op in opts and opts[op]:
1500 1500 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1501 1501 % op.replace("_", "-"))
1502 1502
1503 1503 def graphrevs(repo, nodes, opts):
1504 1504 limit = loglimit(opts)
1505 1505 nodes.reverse()
1506 1506 if limit is not None:
1507 1507 nodes = nodes[:limit]
1508 1508 return graphmod.nodes(repo, nodes)
1509 1509
1510 1510 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1511 1511 join = lambda f: os.path.join(prefix, f)
1512 1512 bad = []
1513 1513 oldbad = match.bad
1514 1514 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1515 1515 names = []
1516 1516 wctx = repo[None]
1517 1517 cca = None
1518 1518 abort, warn = scmutil.checkportabilityalert(ui)
1519 1519 if abort or warn:
1520 1520 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1521 1521 for f in repo.walk(match):
1522 1522 exact = match.exact(f)
1523 1523 if exact or not explicitonly and f not in repo.dirstate:
1524 1524 if cca:
1525 1525 cca(f)
1526 1526 names.append(f)
1527 1527 if ui.verbose or not exact:
1528 1528 ui.status(_('adding %s\n') % match.rel(join(f)))
1529 1529
1530 1530 for subpath in sorted(wctx.substate):
1531 1531 sub = wctx.sub(subpath)
1532 1532 try:
1533 1533 submatch = matchmod.narrowmatcher(subpath, match)
1534 1534 if listsubrepos:
1535 1535 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1536 1536 False))
1537 1537 else:
1538 1538 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1539 1539 True))
1540 1540 except error.LookupError:
1541 1541 ui.status(_("skipping missing subrepository: %s\n")
1542 1542 % join(subpath))
1543 1543
1544 1544 if not dryrun:
1545 1545 rejected = wctx.add(names, prefix)
1546 1546 bad.extend(f for f in rejected if f in match.files())
1547 1547 return bad
1548 1548
1549 1549 def forget(ui, repo, match, prefix, explicitonly):
1550 1550 join = lambda f: os.path.join(prefix, f)
1551 1551 bad = []
1552 1552 oldbad = match.bad
1553 1553 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1554 1554 wctx = repo[None]
1555 1555 forgot = []
1556 1556 s = repo.status(match=match, clean=True)
1557 1557 forget = sorted(s[0] + s[1] + s[3] + s[6])
1558 1558 if explicitonly:
1559 1559 forget = [f for f in forget if match.exact(f)]
1560 1560
1561 1561 for subpath in sorted(wctx.substate):
1562 1562 sub = wctx.sub(subpath)
1563 1563 try:
1564 1564 submatch = matchmod.narrowmatcher(subpath, match)
1565 1565 subbad, subforgot = sub.forget(ui, submatch, prefix)
1566 1566 bad.extend([subpath + '/' + f for f in subbad])
1567 1567 forgot.extend([subpath + '/' + f for f in subforgot])
1568 1568 except error.LookupError:
1569 1569 ui.status(_("skipping missing subrepository: %s\n")
1570 1570 % join(subpath))
1571 1571
1572 1572 if not explicitonly:
1573 1573 for f in match.files():
1574 1574 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1575 1575 if f not in forgot:
1576 1576 if os.path.exists(match.rel(join(f))):
1577 1577 ui.warn(_('not removing %s: '
1578 1578 'file is already untracked\n')
1579 1579 % match.rel(join(f)))
1580 1580 bad.append(f)
1581 1581
1582 1582 for f in forget:
1583 1583 if ui.verbose or not match.exact(f):
1584 1584 ui.status(_('removing %s\n') % match.rel(join(f)))
1585 1585
1586 1586 rejected = wctx.forget(forget, prefix)
1587 1587 bad.extend(f for f in rejected if f in match.files())
1588 1588 forgot.extend(forget)
1589 1589 return bad, forgot
1590 1590
1591 1591 def duplicatecopies(repo, rev, fromrev):
1592 1592 '''reproduce copies from fromrev to rev in the dirstate'''
1593 1593 for dst, src in copies.pathcopies(repo[fromrev], repo[rev]).iteritems():
1594 1594 # copies.pathcopies returns backward renames, so dst might not
1595 1595 # actually be in the dirstate
1596 1596 if repo.dirstate[dst] in "nma":
1597 1597 repo.dirstate.copy(src, dst)
1598 1598
1599 1599 def commit(ui, repo, commitfunc, pats, opts):
1600 1600 '''commit the specified files or all outstanding changes'''
1601 1601 date = opts.get('date')
1602 1602 if date:
1603 1603 opts['date'] = util.parsedate(date)
1604 1604 message = logmessage(ui, opts)
1605 1605
1606 1606 # extract addremove carefully -- this function can be called from a command
1607 1607 # that doesn't support addremove
1608 1608 if opts.get('addremove'):
1609 1609 scmutil.addremove(repo, pats, opts)
1610 1610
1611 1611 return commitfunc(ui, repo, message,
1612 1612 scmutil.match(repo[None], pats, opts), opts)
1613 1613
1614 1614 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1615 1615 ui.note(_('amending changeset %s\n') % old)
1616 1616 base = old.p1()
1617 1617
1618 1618 wlock = lock = newid = None
1619 1619 try:
1620 1620 wlock = repo.wlock()
1621 1621 lock = repo.lock()
1622 1622 tr = repo.transaction('amend')
1623 1623 try:
1624 1624 # See if we got a message from -m or -l, if not, open the editor
1625 1625 # with the message of the changeset to amend
1626 1626 message = logmessage(ui, opts)
1627 1627 # ensure logfile does not conflict with later enforcement of the
1628 1628 # message. potential logfile content has been processed by
1629 1629 # `logmessage` anyway.
1630 1630 opts.pop('logfile')
1631 1631 # First, do a regular commit to record all changes in the working
1632 1632 # directory (if there are any)
1633 1633 ui.callhooks = False
1634 1634 currentbookmark = repo._bookmarkcurrent
1635 1635 try:
1636 1636 repo._bookmarkcurrent = None
1637 1637 opts['message'] = 'temporary amend commit for %s' % old
1638 1638 node = commit(ui, repo, commitfunc, pats, opts)
1639 1639 finally:
1640 1640 repo._bookmarkcurrent = currentbookmark
1641 1641 ui.callhooks = True
1642 1642 ctx = repo[node]
1643 1643
1644 1644 # Participating changesets:
1645 1645 #
1646 1646 # node/ctx o - new (intermediate) commit that contains changes
1647 1647 # | from working dir to go into amending commit
1648 1648 # | (or a workingctx if there were no changes)
1649 1649 # |
1650 1650 # old o - changeset to amend
1651 1651 # |
1652 1652 # base o - parent of amending changeset
1653 1653
1654 1654 # Update extra dict from amended commit (e.g. to preserve graft
1655 1655 # source)
1656 1656 extra.update(old.extra())
1657 1657
1658 1658 # Also update it from the intermediate commit or from the wctx
1659 1659 extra.update(ctx.extra())
1660 1660
1661 1661 if len(old.parents()) > 1:
1662 1662 # ctx.files() isn't reliable for merges, so fall back to the
1663 1663 # slower repo.status() method
1664 1664 files = set([fn for st in repo.status(base, old)[:3]
1665 1665 for fn in st])
1666 1666 else:
1667 1667 files = set(old.files())
1668 1668
1669 1669 # Second, we use either the commit we just did, or if there were no
1670 1670 # changes the parent of the working directory as the version of the
1671 1671 # files in the final amend commit
1672 1672 if node:
1673 1673 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1674 1674
1675 1675 user = ctx.user()
1676 1676 date = ctx.date()
1677 1677 # Recompute copies (avoid recording a -> b -> a)
1678 1678 copied = copies.pathcopies(base, ctx)
1679 1679
1680 1680 # Prune files which were reverted by the updates: if old
1681 1681 # introduced file X and our intermediate commit, node,
1682 1682 # renamed that file, then those two files are the same and
1683 1683 # we can discard X from our list of files. Likewise if X
1684 1684 # was deleted, it's no longer relevant
1685 1685 files.update(ctx.files())
1686 1686
1687 1687 def samefile(f):
1688 1688 if f in ctx.manifest():
1689 1689 a = ctx.filectx(f)
1690 1690 if f in base.manifest():
1691 1691 b = base.filectx(f)
1692 1692 return (not a.cmp(b)
1693 1693 and a.flags() == b.flags())
1694 1694 else:
1695 1695 return False
1696 1696 else:
1697 1697 return f not in base.manifest()
1698 1698 files = [f for f in files if not samefile(f)]
1699 1699
1700 1700 def filectxfn(repo, ctx_, path):
1701 1701 try:
1702 1702 fctx = ctx[path]
1703 1703 flags = fctx.flags()
1704 1704 mctx = context.memfilectx(fctx.path(), fctx.data(),
1705 1705 islink='l' in flags,
1706 1706 isexec='x' in flags,
1707 1707 copied=copied.get(path))
1708 1708 return mctx
1709 1709 except KeyError:
1710 1710 raise IOError
1711 1711 else:
1712 1712 ui.note(_('copying changeset %s to %s\n') % (old, base))
1713 1713
1714 1714 # Use version of files as in the old cset
1715 1715 def filectxfn(repo, ctx_, path):
1716 1716 try:
1717 1717 return old.filectx(path)
1718 1718 except KeyError:
1719 1719 raise IOError
1720 1720
1721 1721 user = opts.get('user') or old.user()
1722 1722 date = opts.get('date') or old.date()
1723 1723 editmsg = False
1724 1724 if not message:
1725 1725 editmsg = True
1726 1726 message = old.description()
1727 1727
1728 1728 pureextra = extra.copy()
1729 1729 extra['amend_source'] = old.hex()
1730 1730
1731 1731 new = context.memctx(repo,
1732 1732 parents=[base.node(), old.p2().node()],
1733 1733 text=message,
1734 1734 files=files,
1735 1735 filectxfn=filectxfn,
1736 1736 user=user,
1737 1737 date=date,
1738 1738 extra=extra)
1739 1739 if editmsg:
1740 1740 new._text = commitforceeditor(repo, new, [])
1741 1741
1742 1742 newdesc = changelog.stripdesc(new.description())
1743 1743 if ((not node)
1744 1744 and newdesc == old.description()
1745 1745 and user == old.user()
1746 1746 and date == old.date()
1747 1747 and pureextra == old.extra()):
1748 1748 # nothing changed. continuing here would create a new node
1749 1749 # anyway because of the amend_source noise.
1750 1750 #
1751 1751 # This not what we expect from amend.
1752 1752 return old.node()
1753 1753
1754 1754 ph = repo.ui.config('phases', 'new-commit', phases.draft)
1755 1755 try:
1756 1756 repo.ui.setconfig('phases', 'new-commit', old.phase())
1757 1757 newid = repo.commitctx(new)
1758 1758 finally:
1759 1759 repo.ui.setconfig('phases', 'new-commit', ph)
1760 1760 if newid != old.node():
1761 1761 # Reroute the working copy parent to the new changeset
1762 1762 repo.setparents(newid, nullid)
1763 1763
1764 1764 # Move bookmarks from old parent to amend commit
1765 1765 bms = repo.nodebookmarks(old.node())
1766 1766 if bms:
1767 1767 marks = repo._bookmarks
1768 1768 for bm in bms:
1769 1769 marks[bm] = newid
1770 1770 marks.write()
1771 1771 #commit the whole amend process
1772 1772 if obsolete._enabled and newid != old.node():
1773 1773 # mark the new changeset as successor of the rewritten one
1774 1774 new = repo[newid]
1775 1775 obs = [(old, (new,))]
1776 1776 if node:
1777 1777 obs.append((ctx, ()))
1778 1778
1779 1779 obsolete.createmarkers(repo, obs)
1780 1780 tr.close()
1781 1781 finally:
1782 1782 tr.release()
1783 1783 if (not obsolete._enabled) and newid != old.node():
1784 1784 # Strip the intermediate commit (if there was one) and the amended
1785 1785 # commit
1786 1786 if node:
1787 1787 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1788 1788 ui.note(_('stripping amended changeset %s\n') % old)
1789 1789 repair.strip(ui, repo, old.node(), topic='amend-backup')
1790 1790 finally:
1791 1791 if newid is None:
1792 1792 repo.dirstate.invalidate()
1793 1793 lockmod.release(lock, wlock)
1794 1794 return newid
1795 1795
1796 1796 def commiteditor(repo, ctx, subs):
1797 1797 if ctx.description():
1798 1798 return ctx.description()
1799 1799 return commitforceeditor(repo, ctx, subs)
1800 1800
1801 1801 def commitforceeditor(repo, ctx, subs):
1802 1802 edittext = []
1803 1803 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1804 1804 if ctx.description():
1805 1805 edittext.append(ctx.description())
1806 1806 edittext.append("")
1807 1807 edittext.append("") # Empty line between message and comments.
1808 1808 edittext.append(_("HG: Enter commit message."
1809 1809 " Lines beginning with 'HG:' are removed."))
1810 1810 edittext.append(_("HG: Leave message empty to abort commit."))
1811 1811 edittext.append("HG: --")
1812 1812 edittext.append(_("HG: user: %s") % ctx.user())
1813 1813 if ctx.p2():
1814 1814 edittext.append(_("HG: branch merge"))
1815 1815 if ctx.branch():
1816 1816 edittext.append(_("HG: branch '%s'") % ctx.branch())
1817 1817 if bookmarks.iscurrent(repo):
1818 1818 edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
1819 1819 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1820 1820 edittext.extend([_("HG: added %s") % f for f in added])
1821 1821 edittext.extend([_("HG: changed %s") % f for f in modified])
1822 1822 edittext.extend([_("HG: removed %s") % f for f in removed])
1823 1823 if not added and not modified and not removed:
1824 1824 edittext.append(_("HG: no files changed"))
1825 1825 edittext.append("")
1826 1826 # run editor in the repository root
1827 1827 olddir = os.getcwd()
1828 1828 os.chdir(repo.root)
1829 1829 text = repo.ui.edit("\n".join(edittext), ctx.user())
1830 1830 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1831 1831 os.chdir(olddir)
1832 1832
1833 1833 if not text.strip():
1834 1834 raise util.Abort(_("empty commit message"))
1835 1835
1836 1836 return text
1837 1837
1838 1838 def commitstatus(repo, node, branch, bheads=None, opts={}):
1839 1839 ctx = repo[node]
1840 1840 parents = ctx.parents()
1841 1841
1842 1842 if (not opts.get('amend') and bheads and node not in bheads and not
1843 1843 [x for x in parents if x.node() in bheads and x.branch() == branch]):
1844 1844 repo.ui.status(_('created new head\n'))
1845 1845 # The message is not printed for initial roots. For the other
1846 1846 # changesets, it is printed in the following situations:
1847 1847 #
1848 1848 # Par column: for the 2 parents with ...
1849 1849 # N: null or no parent
1850 1850 # B: parent is on another named branch
1851 1851 # C: parent is a regular non head changeset
1852 1852 # H: parent was a branch head of the current branch
1853 1853 # Msg column: whether we print "created new head" message
1854 1854 # In the following, it is assumed that there already exists some
1855 1855 # initial branch heads of the current branch, otherwise nothing is
1856 1856 # printed anyway.
1857 1857 #
1858 1858 # Par Msg Comment
1859 1859 # N N y additional topo root
1860 1860 #
1861 1861 # B N y additional branch root
1862 1862 # C N y additional topo head
1863 1863 # H N n usual case
1864 1864 #
1865 1865 # B B y weird additional branch root
1866 1866 # C B y branch merge
1867 1867 # H B n merge with named branch
1868 1868 #
1869 1869 # C C y additional head from merge
1870 1870 # C H n merge with a head
1871 1871 #
1872 1872 # H H n head merge: head count decreases
1873 1873
1874 1874 if not opts.get('close_branch'):
1875 1875 for r in parents:
1876 1876 if r.closesbranch() and r.branch() == branch:
1877 1877 repo.ui.status(_('reopening closed branch head %d\n') % r)
1878 1878
1879 1879 if repo.ui.debugflag:
1880 1880 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
1881 1881 elif repo.ui.verbose:
1882 1882 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
1883 1883
1884 1884 def revert(ui, repo, ctx, parents, *pats, **opts):
1885 1885 parent, p2 = parents
1886 1886 node = ctx.node()
1887 1887
1888 1888 mf = ctx.manifest()
1889 1889 if node == parent:
1890 1890 pmf = mf
1891 1891 else:
1892 1892 pmf = None
1893 1893
1894 1894 # need all matching names in dirstate and manifest of target rev,
1895 1895 # so have to walk both. do not print errors if files exist in one
1896 1896 # but not other.
1897 1897
1898 1898 names = {}
1899 1899
1900 1900 wlock = repo.wlock()
1901 1901 try:
1902 1902 # walk dirstate.
1903 1903
1904 1904 m = scmutil.match(repo[None], pats, opts)
1905 1905 m.bad = lambda x, y: False
1906 1906 for abs in repo.walk(m):
1907 1907 names[abs] = m.rel(abs), m.exact(abs)
1908 1908
1909 1909 # walk target manifest.
1910 1910
1911 1911 def badfn(path, msg):
1912 1912 if path in names:
1913 1913 return
1914 1914 if path in ctx.substate:
1915 1915 return
1916 1916 path_ = path + '/'
1917 1917 for f in names:
1918 1918 if f.startswith(path_):
1919 1919 return
1920 1920 ui.warn("%s: %s\n" % (m.rel(path), msg))
1921 1921
1922 1922 m = scmutil.match(ctx, pats, opts)
1923 1923 m.bad = badfn
1924 1924 for abs in ctx.walk(m):
1925 1925 if abs not in names:
1926 1926 names[abs] = m.rel(abs), m.exact(abs)
1927 1927
1928 1928 # get the list of subrepos that must be reverted
1929 1929 targetsubs = sorted(s for s in ctx.substate if m(s))
1930 1930 m = scmutil.matchfiles(repo, names)
1931 1931 changes = repo.status(match=m)[:4]
1932 1932 modified, added, removed, deleted = map(set, changes)
1933 1933
1934 1934 # if f is a rename, also revert the source
1935 1935 cwd = repo.getcwd()
1936 1936 for f in added:
1937 1937 src = repo.dirstate.copied(f)
1938 1938 if src and src not in names and repo.dirstate[src] == 'r':
1939 1939 removed.add(src)
1940 1940 names[src] = (repo.pathto(src, cwd), True)
1941 1941
1942 1942 def removeforget(abs):
1943 1943 if repo.dirstate[abs] == 'a':
1944 1944 return _('forgetting %s\n')
1945 1945 return _('removing %s\n')
1946 1946
1947 1947 revert = ([], _('reverting %s\n'))
1948 1948 add = ([], _('adding %s\n'))
1949 1949 remove = ([], removeforget)
1950 1950 undelete = ([], _('undeleting %s\n'))
1951 1951
1952 1952 disptable = (
1953 1953 # dispatch table:
1954 1954 # file state
1955 1955 # action if in target manifest
1956 1956 # action if not in target manifest
1957 1957 # make backup if in target manifest
1958 1958 # make backup if not in target manifest
1959 1959 (modified, revert, remove, True, True),
1960 1960 (added, revert, remove, True, False),
1961 1961 (removed, undelete, None, False, False),
1962 1962 (deleted, revert, remove, False, False),
1963 1963 )
1964 1964
1965 1965 for abs, (rel, exact) in sorted(names.items()):
1966 1966 mfentry = mf.get(abs)
1967 1967 target = repo.wjoin(abs)
1968 1968 def handle(xlist, dobackup):
1969 1969 xlist[0].append(abs)
1970 1970 if (dobackup and not opts.get('no_backup') and
1971 1971 os.path.lexists(target)):
1972 1972 bakname = "%s.orig" % rel
1973 1973 ui.note(_('saving current version of %s as %s\n') %
1974 1974 (rel, bakname))
1975 1975 if not opts.get('dry_run'):
1976 1976 util.rename(target, bakname)
1977 1977 if ui.verbose or not exact:
1978 1978 msg = xlist[1]
1979 1979 if not isinstance(msg, basestring):
1980 1980 msg = msg(abs)
1981 1981 ui.status(msg % rel)
1982 1982 for table, hitlist, misslist, backuphit, backupmiss in disptable:
1983 1983 if abs not in table:
1984 1984 continue
1985 1985 # file has changed in dirstate
1986 1986 if mfentry:
1987 1987 handle(hitlist, backuphit)
1988 1988 elif misslist is not None:
1989 1989 handle(misslist, backupmiss)
1990 1990 break
1991 1991 else:
1992 1992 if abs not in repo.dirstate:
1993 1993 if mfentry:
1994 1994 handle(add, True)
1995 1995 elif exact:
1996 1996 ui.warn(_('file not managed: %s\n') % rel)
1997 1997 continue
1998 1998 # file has not changed in dirstate
1999 1999 if node == parent:
2000 2000 if exact:
2001 2001 ui.warn(_('no changes needed to %s\n') % rel)
2002 2002 continue
2003 2003 if pmf is None:
2004 2004 # only need parent manifest in this unlikely case,
2005 2005 # so do not read by default
2006 2006 pmf = repo[parent].manifest()
2007 2007 if abs in pmf and mfentry:
2008 2008 # if version of file is same in parent and target
2009 2009 # manifests, do nothing
2010 2010 if (pmf[abs] != mfentry or
2011 2011 pmf.flags(abs) != mf.flags(abs)):
2012 2012 handle(revert, False)
2013 2013 else:
2014 2014 handle(remove, False)
2015 2015
2016 2016 if not opts.get('dry_run'):
2017 2017 def checkout(f):
2018 2018 fc = ctx[f]
2019 2019 repo.wwrite(f, fc.data(), fc.flags())
2020 2020
2021 2021 audit_path = scmutil.pathauditor(repo.root)
2022 2022 for f in remove[0]:
2023 2023 if repo.dirstate[f] == 'a':
2024 2024 repo.dirstate.drop(f)
2025 2025 continue
2026 2026 audit_path(f)
2027 2027 try:
2028 2028 util.unlinkpath(repo.wjoin(f))
2029 2029 except OSError:
2030 2030 pass
2031 2031 repo.dirstate.remove(f)
2032 2032
2033 2033 normal = None
2034 2034 if node == parent:
2035 2035 # We're reverting to our parent. If possible, we'd like status
2036 2036 # to report the file as clean. We have to use normallookup for
2037 2037 # merges to avoid losing information about merged/dirty files.
2038 2038 if p2 != nullid:
2039 2039 normal = repo.dirstate.normallookup
2040 2040 else:
2041 2041 normal = repo.dirstate.normal
2042 2042 for f in revert[0]:
2043 2043 checkout(f)
2044 2044 if normal:
2045 2045 normal(f)
2046 2046
2047 2047 for f in add[0]:
2048 2048 checkout(f)
2049 2049 repo.dirstate.add(f)
2050 2050
2051 2051 normal = repo.dirstate.normallookup
2052 2052 if node == parent and p2 == nullid:
2053 2053 normal = repo.dirstate.normal
2054 2054 for f in undelete[0]:
2055 2055 checkout(f)
2056 2056 normal(f)
2057 2057
2058 copied = copies.pathcopies(repo[parent], ctx)
2059
2060 for f in add[0] + undelete[0] + revert[0]:
2061 if f in copied:
2062 repo.dirstate.copy(copied[f], f)
2063
2058 2064 if targetsubs:
2059 2065 # Revert the subrepos on the revert list
2060 2066 for sub in targetsubs:
2061 2067 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
2062 2068 finally:
2063 2069 wlock.release()
2064 2070
2065 2071 def command(table):
2066 2072 '''returns a function object bound to table which can be used as
2067 2073 a decorator for populating table as a command table'''
2068 2074
2069 2075 def cmd(name, options=(), synopsis=None):
2070 2076 def decorator(func):
2071 2077 if synopsis:
2072 2078 table[name] = func, list(options), synopsis
2073 2079 else:
2074 2080 table[name] = func, list(options)
2075 2081 return func
2076 2082 return decorator
2077 2083
2078 2084 return cmd
@@ -1,815 +1,815 b''
1 1 # dirstate.py - working directory tracking for 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 import errno
8 8
9 9 from node import nullid
10 10 from i18n import _
11 11 import scmutil, util, ignore, osutil, parsers, encoding
12 12 import os, stat, errno, gc
13 13
14 14 propertycache = util.propertycache
15 15 filecache = scmutil.filecache
16 16 _rangemask = 0x7fffffff
17 17
18 18 class repocache(filecache):
19 19 """filecache for files in .hg/"""
20 20 def join(self, obj, fname):
21 21 return obj._opener.join(fname)
22 22
23 23 class rootcache(filecache):
24 24 """filecache for files in the repository root"""
25 25 def join(self, obj, fname):
26 26 return obj._join(fname)
27 27
28 28 class dirstate(object):
29 29
30 30 def __init__(self, opener, ui, root, validate):
31 31 '''Create a new dirstate object.
32 32
33 33 opener is an open()-like callable that can be used to open the
34 34 dirstate file; root is the root of the directory tracked by
35 35 the dirstate.
36 36 '''
37 37 self._opener = opener
38 38 self._validate = validate
39 39 self._root = root
40 40 self._rootdir = os.path.join(root, '')
41 41 self._dirty = False
42 42 self._dirtypl = False
43 43 self._lastnormaltime = 0
44 44 self._ui = ui
45 45 self._filecache = {}
46 46
47 47 @propertycache
48 48 def _map(self):
49 49 '''Return the dirstate contents as a map from filename to
50 50 (state, mode, size, time).'''
51 51 self._read()
52 52 return self._map
53 53
54 54 @propertycache
55 55 def _copymap(self):
56 56 self._read()
57 57 return self._copymap
58 58
59 59 @propertycache
60 60 def _foldmap(self):
61 61 f = {}
62 62 for name, s in self._map.iteritems():
63 63 if s[0] != 'r':
64 64 f[util.normcase(name)] = name
65 65 for name in self._dirs:
66 66 f[util.normcase(name)] = name
67 67 f['.'] = '.' # prevents useless util.fspath() invocation
68 68 return f
69 69
70 70 @repocache('branch')
71 71 def _branch(self):
72 72 try:
73 73 return self._opener.read("branch").strip() or "default"
74 74 except IOError, inst:
75 75 if inst.errno != errno.ENOENT:
76 76 raise
77 77 return "default"
78 78
79 79 @propertycache
80 80 def _pl(self):
81 81 try:
82 82 fp = self._opener("dirstate")
83 83 st = fp.read(40)
84 84 fp.close()
85 85 l = len(st)
86 86 if l == 40:
87 87 return st[:20], st[20:40]
88 88 elif l > 0 and l < 40:
89 89 raise util.Abort(_('working directory state appears damaged!'))
90 90 except IOError, err:
91 91 if err.errno != errno.ENOENT:
92 92 raise
93 93 return [nullid, nullid]
94 94
95 95 @propertycache
96 96 def _dirs(self):
97 97 return scmutil.dirs(self._map, 'r')
98 98
99 99 def dirs(self):
100 100 return self._dirs
101 101
102 102 @rootcache('.hgignore')
103 103 def _ignore(self):
104 104 files = [self._join('.hgignore')]
105 105 for name, path in self._ui.configitems("ui"):
106 106 if name == 'ignore' or name.startswith('ignore.'):
107 107 files.append(util.expandpath(path))
108 108 return ignore.ignore(self._root, files, self._ui.warn)
109 109
110 110 @propertycache
111 111 def _slash(self):
112 112 return self._ui.configbool('ui', 'slash') and os.sep != '/'
113 113
114 114 @propertycache
115 115 def _checklink(self):
116 116 return util.checklink(self._root)
117 117
118 118 @propertycache
119 119 def _checkexec(self):
120 120 return util.checkexec(self._root)
121 121
122 122 @propertycache
123 123 def _checkcase(self):
124 124 return not util.checkcase(self._join('.hg'))
125 125
126 126 def _join(self, f):
127 127 # much faster than os.path.join()
128 128 # it's safe because f is always a relative path
129 129 return self._rootdir + f
130 130
131 131 def flagfunc(self, buildfallback):
132 132 if self._checklink and self._checkexec:
133 133 def f(x):
134 134 try:
135 135 st = os.lstat(self._join(x))
136 136 if util.statislink(st):
137 137 return 'l'
138 138 if util.statisexec(st):
139 139 return 'x'
140 140 except OSError:
141 141 pass
142 142 return ''
143 143 return f
144 144
145 145 fallback = buildfallback()
146 146 if self._checklink:
147 147 def f(x):
148 148 if os.path.islink(self._join(x)):
149 149 return 'l'
150 150 if 'x' in fallback(x):
151 151 return 'x'
152 152 return ''
153 153 return f
154 154 if self._checkexec:
155 155 def f(x):
156 156 if 'l' in fallback(x):
157 157 return 'l'
158 158 if util.isexec(self._join(x)):
159 159 return 'x'
160 160 return ''
161 161 return f
162 162 else:
163 163 return fallback
164 164
165 165 def getcwd(self):
166 166 cwd = os.getcwd()
167 167 if cwd == self._root:
168 168 return ''
169 169 # self._root ends with a path separator if self._root is '/' or 'C:\'
170 170 rootsep = self._root
171 171 if not util.endswithsep(rootsep):
172 172 rootsep += os.sep
173 173 if cwd.startswith(rootsep):
174 174 return cwd[len(rootsep):]
175 175 else:
176 176 # we're outside the repo. return an absolute path.
177 177 return cwd
178 178
179 179 def pathto(self, f, cwd=None):
180 180 if cwd is None:
181 181 cwd = self.getcwd()
182 182 path = util.pathto(self._root, cwd, f)
183 183 if self._slash:
184 184 return util.normpath(path)
185 185 return path
186 186
187 187 def __getitem__(self, key):
188 188 '''Return the current state of key (a filename) in the dirstate.
189 189
190 190 States are:
191 191 n normal
192 192 m needs merging
193 193 r marked for removal
194 194 a marked for addition
195 195 ? not tracked
196 196 '''
197 197 return self._map.get(key, ("?",))[0]
198 198
199 199 def __contains__(self, key):
200 200 return key in self._map
201 201
202 202 def __iter__(self):
203 203 for x in sorted(self._map):
204 204 yield x
205 205
206 206 def iteritems(self):
207 207 return self._map.iteritems()
208 208
209 209 def parents(self):
210 210 return [self._validate(p) for p in self._pl]
211 211
212 212 def p1(self):
213 213 return self._validate(self._pl[0])
214 214
215 215 def p2(self):
216 216 return self._validate(self._pl[1])
217 217
218 218 def branch(self):
219 219 return encoding.tolocal(self._branch)
220 220
221 221 def setparents(self, p1, p2=nullid):
222 222 """Set dirstate parents to p1 and p2.
223 223
224 224 When moving from two parents to one, 'm' merged entries a
225 225 adjusted to normal and previous copy records discarded and
226 226 returned by the call.
227 227
228 228 See localrepo.setparents()
229 229 """
230 230 self._dirty = self._dirtypl = True
231 231 oldp2 = self._pl[1]
232 232 self._pl = p1, p2
233 233 copies = {}
234 234 if oldp2 != nullid and p2 == nullid:
235 235 # Discard 'm' markers when moving away from a merge state
236 236 for f, s in self._map.iteritems():
237 237 if s[0] == 'm':
238 238 if f in self._copymap:
239 239 copies[f] = self._copymap[f]
240 240 self.normallookup(f)
241 241 return copies
242 242
243 243 def setbranch(self, branch):
244 244 self._branch = encoding.fromlocal(branch)
245 245 f = self._opener('branch', 'w', atomictemp=True)
246 246 try:
247 247 f.write(self._branch + '\n')
248 248 f.close()
249 249
250 250 # make sure filecache has the correct stat info for _branch after
251 251 # replacing the underlying file
252 252 ce = self._filecache['_branch']
253 253 if ce:
254 254 ce.refresh()
255 255 except: # re-raises
256 256 f.discard()
257 257 raise
258 258
259 259 def _read(self):
260 260 self._map = {}
261 261 self._copymap = {}
262 262 try:
263 263 st = self._opener.read("dirstate")
264 264 except IOError, err:
265 265 if err.errno != errno.ENOENT:
266 266 raise
267 267 return
268 268 if not st:
269 269 return
270 270
271 271 # Python's garbage collector triggers a GC each time a certain number
272 272 # of container objects (the number being defined by
273 273 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
274 274 # for each file in the dirstate. The C version then immediately marks
275 275 # them as not to be tracked by the collector. However, this has no
276 276 # effect on when GCs are triggered, only on what objects the GC looks
277 277 # into. This means that O(number of files) GCs are unavoidable.
278 278 # Depending on when in the process's lifetime the dirstate is parsed,
279 279 # this can get very expensive. As a workaround, disable GC while
280 280 # parsing the dirstate.
281 281 gcenabled = gc.isenabled()
282 282 gc.disable()
283 283 try:
284 284 p = parsers.parse_dirstate(self._map, self._copymap, st)
285 285 finally:
286 286 if gcenabled:
287 287 gc.enable()
288 288 if not self._dirtypl:
289 289 self._pl = p
290 290
291 291 def invalidate(self):
292 292 for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs",
293 293 "_ignore"):
294 294 if a in self.__dict__:
295 295 delattr(self, a)
296 296 self._lastnormaltime = 0
297 297 self._dirty = False
298 298
299 299 def copy(self, source, dest):
300 300 """Mark dest as a copy of source. Unmark dest if source is None."""
301 301 if source == dest:
302 302 return
303 303 self._dirty = True
304 304 if source is not None:
305 305 self._copymap[dest] = source
306 306 elif dest in self._copymap:
307 307 del self._copymap[dest]
308 308
309 309 def copied(self, file):
310 310 return self._copymap.get(file, None)
311 311
312 312 def copies(self):
313 313 return self._copymap
314 314
315 315 def _droppath(self, f):
316 316 if self[f] not in "?r" and "_dirs" in self.__dict__:
317 317 self._dirs.delpath(f)
318 318
319 319 def _addpath(self, f, state, mode, size, mtime):
320 320 oldstate = self[f]
321 321 if state == 'a' or oldstate == 'r':
322 322 scmutil.checkfilename(f)
323 323 if f in self._dirs:
324 324 raise util.Abort(_('directory %r already in dirstate') % f)
325 325 # shadows
326 326 for d in scmutil.finddirs(f):
327 327 if d in self._dirs:
328 328 break
329 329 if d in self._map and self[d] != 'r':
330 330 raise util.Abort(
331 331 _('file %r in dirstate clashes with %r') % (d, f))
332 332 if oldstate in "?r" and "_dirs" in self.__dict__:
333 333 self._dirs.addpath(f)
334 334 self._dirty = True
335 335 self._map[f] = (state, mode, size, mtime)
336 336
337 337 def normal(self, f):
338 338 '''Mark a file normal and clean.'''
339 339 s = os.lstat(self._join(f))
340 340 mtime = int(s.st_mtime)
341 341 self._addpath(f, 'n', s.st_mode,
342 342 s.st_size & _rangemask, mtime & _rangemask)
343 343 if f in self._copymap:
344 344 del self._copymap[f]
345 345 if mtime > self._lastnormaltime:
346 346 # Remember the most recent modification timeslot for status(),
347 347 # to make sure we won't miss future size-preserving file content
348 348 # modifications that happen within the same timeslot.
349 349 self._lastnormaltime = mtime
350 350
351 351 def normallookup(self, f):
352 352 '''Mark a file normal, but possibly dirty.'''
353 353 if self._pl[1] != nullid and f in self._map:
354 354 # if there is a merge going on and the file was either
355 355 # in state 'm' (-1) or coming from other parent (-2) before
356 356 # being removed, restore that state.
357 357 entry = self._map[f]
358 358 if entry[0] == 'r' and entry[2] in (-1, -2):
359 359 source = self._copymap.get(f)
360 360 if entry[2] == -1:
361 361 self.merge(f)
362 362 elif entry[2] == -2:
363 363 self.otherparent(f)
364 364 if source:
365 365 self.copy(source, f)
366 366 return
367 367 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
368 368 return
369 369 self._addpath(f, 'n', 0, -1, -1)
370 370 if f in self._copymap:
371 371 del self._copymap[f]
372 372
373 373 def otherparent(self, f):
374 374 '''Mark as coming from the other parent, always dirty.'''
375 375 if self._pl[1] == nullid:
376 376 raise util.Abort(_("setting %r to other parent "
377 377 "only allowed in merges") % f)
378 378 self._addpath(f, 'n', 0, -2, -1)
379 379 if f in self._copymap:
380 380 del self._copymap[f]
381 381
382 382 def add(self, f):
383 383 '''Mark a file added.'''
384 384 self._addpath(f, 'a', 0, -1, -1)
385 385 if f in self._copymap:
386 386 del self._copymap[f]
387 387
388 388 def remove(self, f):
389 389 '''Mark a file removed.'''
390 390 self._dirty = True
391 391 self._droppath(f)
392 392 size = 0
393 393 if self._pl[1] != nullid and f in self._map:
394 394 # backup the previous state
395 395 entry = self._map[f]
396 396 if entry[0] == 'm': # merge
397 397 size = -1
398 398 elif entry[0] == 'n' and entry[2] == -2: # other parent
399 399 size = -2
400 400 self._map[f] = ('r', 0, size, 0)
401 401 if size == 0 and f in self._copymap:
402 402 del self._copymap[f]
403 403
404 404 def merge(self, f):
405 405 '''Mark a file merged.'''
406 406 if self._pl[1] == nullid:
407 407 return self.normallookup(f)
408 408 s = os.lstat(self._join(f))
409 409 self._addpath(f, 'm', s.st_mode,
410 410 s.st_size & _rangemask, int(s.st_mtime) & _rangemask)
411 411 if f in self._copymap:
412 412 del self._copymap[f]
413 413
414 414 def drop(self, f):
415 415 '''Drop a file from the dirstate'''
416 416 if f in self._map:
417 417 self._dirty = True
418 418 self._droppath(f)
419 419 del self._map[f]
420 420
421 421 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
422 422 normed = util.normcase(path)
423 423 folded = self._foldmap.get(normed, None)
424 424 if folded is None:
425 425 if isknown:
426 426 folded = path
427 427 else:
428 428 if exists is None:
429 429 exists = os.path.lexists(os.path.join(self._root, path))
430 430 if not exists:
431 431 # Maybe a path component exists
432 432 if not ignoremissing and '/' in path:
433 433 d, f = path.rsplit('/', 1)
434 434 d = self._normalize(d, isknown, ignoremissing, None)
435 435 folded = d + "/" + f
436 436 else:
437 437 # No path components, preserve original case
438 438 folded = path
439 439 else:
440 440 # recursively normalize leading directory components
441 441 # against dirstate
442 442 if '/' in normed:
443 443 d, f = normed.rsplit('/', 1)
444 444 d = self._normalize(d, isknown, ignoremissing, True)
445 445 r = self._root + "/" + d
446 446 folded = d + "/" + util.fspath(f, r)
447 447 else:
448 448 folded = util.fspath(normed, self._root)
449 449 self._foldmap[normed] = folded
450 450
451 451 return folded
452 452
453 453 def normalize(self, path, isknown=False, ignoremissing=False):
454 454 '''
455 455 normalize the case of a pathname when on a casefolding filesystem
456 456
457 457 isknown specifies whether the filename came from walking the
458 458 disk, to avoid extra filesystem access.
459 459
460 460 If ignoremissing is True, missing path are returned
461 461 unchanged. Otherwise, we try harder to normalize possibly
462 462 existing path components.
463 463
464 464 The normalized case is determined based on the following precedence:
465 465
466 466 - version of name already stored in the dirstate
467 467 - version of name stored on disk
468 468 - version provided via command arguments
469 469 '''
470 470
471 471 if self._checkcase:
472 472 return self._normalize(path, isknown, ignoremissing)
473 473 return path
474 474
475 475 def clear(self):
476 476 self._map = {}
477 477 if "_dirs" in self.__dict__:
478 478 delattr(self, "_dirs")
479 479 self._copymap = {}
480 480 self._pl = [nullid, nullid]
481 481 self._lastnormaltime = 0
482 482 self._dirty = True
483 483
484 484 def rebuild(self, parent, allfiles, changedfiles=None):
485 485 changedfiles = changedfiles or allfiles
486 486 oldmap = self._map
487 487 self.clear()
488 488 for f in allfiles:
489 489 if f not in changedfiles:
490 490 self._map[f] = oldmap[f]
491 491 else:
492 492 if 'x' in allfiles.flags(f):
493 493 self._map[f] = ('n', 0777, -1, 0)
494 494 else:
495 495 self._map[f] = ('n', 0666, -1, 0)
496 496 self._pl = (parent, nullid)
497 497 self._dirty = True
498 498
499 499 def write(self):
500 500 if not self._dirty:
501 501 return
502 502 st = self._opener("dirstate", "w", atomictemp=True)
503 503
504 504 def finish(s):
505 505 st.write(s)
506 506 st.close()
507 507 self._lastnormaltime = 0
508 508 self._dirty = self._dirtypl = False
509 509
510 510 # use the modification time of the newly created temporary file as the
511 511 # filesystem's notion of 'now'
512 512 now = util.fstat(st).st_mtime
513 513 finish(parsers.pack_dirstate(self._map, self._copymap, self._pl, now))
514 514
515 515 def _dirignore(self, f):
516 516 if f == '.':
517 517 return False
518 518 if self._ignore(f):
519 519 return True
520 520 for p in scmutil.finddirs(f):
521 521 if self._ignore(p):
522 522 return True
523 523 return False
524 524
525 525 def walk(self, match, subrepos, unknown, ignored):
526 526 '''
527 527 Walk recursively through the directory tree, finding all files
528 528 matched by match.
529 529
530 530 Return a dict mapping filename to stat-like object (either
531 531 mercurial.osutil.stat instance or return value of os.stat()).
532 532 '''
533 533
534 534 def fwarn(f, msg):
535 535 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
536 536 return False
537 537
538 538 def badtype(mode):
539 539 kind = _('unknown')
540 540 if stat.S_ISCHR(mode):
541 541 kind = _('character device')
542 542 elif stat.S_ISBLK(mode):
543 543 kind = _('block device')
544 544 elif stat.S_ISFIFO(mode):
545 545 kind = _('fifo')
546 546 elif stat.S_ISSOCK(mode):
547 547 kind = _('socket')
548 548 elif stat.S_ISDIR(mode):
549 549 kind = _('directory')
550 550 return _('unsupported file type (type is %s)') % kind
551 551
552 552 ignore = self._ignore
553 553 dirignore = self._dirignore
554 554 if ignored:
555 555 ignore = util.never
556 556 dirignore = util.never
557 557 elif not unknown:
558 558 # if unknown and ignored are False, skip step 2
559 559 ignore = util.always
560 560 dirignore = util.always
561 561
562 562 matchfn = match.matchfn
563 563 matchalways = match.always()
564 564 badfn = match.bad
565 565 dmap = self._map
566 566 normpath = util.normpath
567 567 listdir = osutil.listdir
568 568 lstat = os.lstat
569 569 getkind = stat.S_IFMT
570 570 dirkind = stat.S_IFDIR
571 571 regkind = stat.S_IFREG
572 572 lnkkind = stat.S_IFLNK
573 573 join = self._join
574 574 work = []
575 575 wadd = work.append
576 576
577 577 exact = skipstep3 = False
578 578 if matchfn == match.exact: # match.exact
579 579 exact = True
580 580 dirignore = util.always # skip step 2
581 581 elif match.files() and not match.anypats(): # match.match, no patterns
582 582 skipstep3 = True
583 583
584 584 if not exact and self._checkcase:
585 585 normalize = self._normalize
586 586 skipstep3 = False
587 587 else:
588 588 normalize = None
589 589
590 590 files = sorted(match.files())
591 591 subrepos.sort()
592 592 i, j = 0, 0
593 593 while i < len(files) and j < len(subrepos):
594 594 subpath = subrepos[j] + "/"
595 595 if files[i] < subpath:
596 596 i += 1
597 597 continue
598 598 while i < len(files) and files[i].startswith(subpath):
599 599 del files[i]
600 600 j += 1
601 601
602 602 if not files or '.' in files:
603 603 files = ['']
604 604 results = dict.fromkeys(subrepos)
605 605 results['.hg'] = None
606 606
607 607 # step 1: find all explicit files
608 608 for ff in files:
609 609 if normalize:
610 610 nf = normalize(normpath(ff), False, True)
611 611 else:
612 612 nf = normpath(ff)
613 613 if nf in results:
614 614 continue
615 615
616 616 try:
617 617 st = lstat(join(nf))
618 618 kind = getkind(st.st_mode)
619 619 if kind == dirkind:
620 620 skipstep3 = False
621 621 if nf in dmap:
622 622 #file deleted on disk but still in dirstate
623 623 results[nf] = None
624 624 match.dir(nf)
625 625 if not dirignore(nf):
626 626 wadd(nf)
627 627 elif kind == regkind or kind == lnkkind:
628 628 results[nf] = st
629 629 else:
630 630 badfn(ff, badtype(kind))
631 631 if nf in dmap:
632 632 results[nf] = None
633 633 except OSError, inst:
634 634 if nf in dmap: # does it exactly match a file?
635 635 results[nf] = None
636 636 else: # does it match a directory?
637 637 prefix = nf + "/"
638 638 for fn in dmap:
639 639 if fn.startswith(prefix):
640 640 match.dir(nf)
641 641 skipstep3 = False
642 642 break
643 643 else:
644 644 badfn(ff, inst.strerror)
645 645
646 646 # step 2: visit subdirectories
647 647 while work:
648 648 nd = work.pop()
649 649 skip = None
650 650 if nd == '.':
651 651 nd = ''
652 652 else:
653 653 skip = '.hg'
654 654 try:
655 655 entries = listdir(join(nd), stat=True, skip=skip)
656 656 except OSError, inst:
657 657 if inst.errno in (errno.EACCES, errno.ENOENT):
658 658 fwarn(nd, inst.strerror)
659 659 continue
660 660 raise
661 661 for f, kind, st in entries:
662 662 if normalize:
663 663 nf = normalize(nd and (nd + "/" + f) or f, True, True)
664 664 else:
665 665 nf = nd and (nd + "/" + f) or f
666 666 if nf not in results:
667 667 if kind == dirkind:
668 668 if not ignore(nf):
669 669 match.dir(nf)
670 670 wadd(nf)
671 671 if nf in dmap and (matchalways or matchfn(nf)):
672 672 results[nf] = None
673 673 elif kind == regkind or kind == lnkkind:
674 674 if nf in dmap:
675 675 if matchalways or matchfn(nf):
676 676 results[nf] = st
677 677 elif (matchalways or matchfn(nf)) and not ignore(nf):
678 678 results[nf] = st
679 679 elif nf in dmap and (matchalways or matchfn(nf)):
680 680 results[nf] = None
681 681
682 682 for s in subrepos:
683 683 del results[s]
684 684 del results['.hg']
685 685
686 686 # step 3: report unseen items in the dmap hash
687 687 if not skipstep3 and not exact:
688 688 if not results and matchalways:
689 689 visit = dmap.keys()
690 690 else:
691 691 visit = [f for f in dmap if f not in results and matchfn(f)]
692 692 visit.sort()
693 693
694 694 if unknown:
695 695 # unknown == True means we walked the full directory tree above.
696 696 # So if a file is not seen it was either a) not matching matchfn
697 697 # b) ignored, c) missing, or d) under a symlink directory.
698 698 audit_path = scmutil.pathauditor(self._root)
699 699
700 700 for nf in iter(visit):
701 701 # Report ignored items in the dmap as long as they are not
702 702 # under a symlink directory.
703 if ignore(nf) and audit_path.check(nf):
703 if audit_path.check(nf):
704 704 try:
705 705 results[nf] = lstat(join(nf))
706 706 except OSError:
707 707 # file doesn't exist
708 708 results[nf] = None
709 709 else:
710 710 # It's either missing or under a symlink directory
711 711 results[nf] = None
712 712 else:
713 713 # We may not have walked the full directory tree above,
714 714 # so stat everything we missed.
715 715 nf = iter(visit).next
716 716 for st in util.statfiles([join(i) for i in visit]):
717 717 results[nf()] = st
718 718 return results
719 719
720 720 def status(self, match, subrepos, ignored, clean, unknown):
721 721 '''Determine the status of the working copy relative to the
722 722 dirstate and return a tuple of lists (unsure, modified, added,
723 723 removed, deleted, unknown, ignored, clean), where:
724 724
725 725 unsure:
726 726 files that might have been modified since the dirstate was
727 727 written, but need to be read to be sure (size is the same
728 728 but mtime differs)
729 729 modified:
730 730 files that have definitely been modified since the dirstate
731 731 was written (different size or mode)
732 732 added:
733 733 files that have been explicitly added with hg add
734 734 removed:
735 735 files that have been explicitly removed with hg remove
736 736 deleted:
737 737 files that have been deleted through other means ("missing")
738 738 unknown:
739 739 files not in the dirstate that are not ignored
740 740 ignored:
741 741 files not in the dirstate that are ignored
742 742 (by _dirignore())
743 743 clean:
744 744 files that have definitely not been modified since the
745 745 dirstate was written
746 746 '''
747 747 listignored, listclean, listunknown = ignored, clean, unknown
748 748 lookup, modified, added, unknown, ignored = [], [], [], [], []
749 749 removed, deleted, clean = [], [], []
750 750
751 751 dmap = self._map
752 752 ladd = lookup.append # aka "unsure"
753 753 madd = modified.append
754 754 aadd = added.append
755 755 uadd = unknown.append
756 756 iadd = ignored.append
757 757 radd = removed.append
758 758 dadd = deleted.append
759 759 cadd = clean.append
760 760 mexact = match.exact
761 761 dirignore = self._dirignore
762 762 checkexec = self._checkexec
763 763 checklink = self._checklink
764 764 copymap = self._copymap
765 765 lastnormaltime = self._lastnormaltime
766 766
767 767 lnkkind = stat.S_IFLNK
768 768
769 769 for fn, st in self.walk(match, subrepos, listunknown,
770 770 listignored).iteritems():
771 771 if fn not in dmap:
772 772 if (listignored or mexact(fn)) and dirignore(fn):
773 773 if listignored:
774 774 iadd(fn)
775 775 elif listunknown:
776 776 uadd(fn)
777 777 continue
778 778
779 779 state, mode, size, time = dmap[fn]
780 780
781 781 if not st and state in "nma":
782 782 dadd(fn)
783 783 elif state == 'n':
784 784 # The "mode & lnkkind != lnkkind or self._checklink"
785 785 # lines are an expansion of "islink => checklink"
786 786 # where islink means "is this a link?" and checklink
787 787 # means "can we check links?".
788 788 mtime = int(st.st_mtime)
789 789 if (size >= 0 and
790 790 ((size != st.st_size and size != st.st_size & _rangemask)
791 791 or ((mode ^ st.st_mode) & 0100 and checkexec))
792 792 and (mode & lnkkind != lnkkind or checklink)
793 793 or size == -2 # other parent
794 794 or fn in copymap):
795 795 madd(fn)
796 796 elif ((time != mtime and time != mtime & _rangemask)
797 797 and (mode & lnkkind != lnkkind or checklink)):
798 798 ladd(fn)
799 799 elif mtime == lastnormaltime:
800 800 # fn may have been changed in the same timeslot without
801 801 # changing its size. This can happen if we quickly do
802 802 # multiple commits in a single transaction.
803 803 # Force lookup, so we don't miss such a racy file change.
804 804 ladd(fn)
805 805 elif listclean:
806 806 cadd(fn)
807 807 elif state == 'm':
808 808 madd(fn)
809 809 elif state == 'a':
810 810 aadd(fn)
811 811 elif state == 'r':
812 812 radd(fn)
813 813
814 814 return (lookup, modified, added, removed, deleted, unknown, ignored,
815 815 clean)
@@ -1,126 +1,136 b''
1 1 $ hg init
2 2
3 3 Issue562: .hgignore requires newline at end:
4 4
5 5 $ touch foo
6 6 $ touch bar
7 7 $ touch baz
8 8 $ cat > makeignore.py <<EOF
9 9 > f = open(".hgignore", "w")
10 10 > f.write("ignore\n")
11 11 > f.write("foo\n")
12 12 > # No EOL here
13 13 > f.write("bar")
14 14 > f.close()
15 15 > EOF
16 16
17 17 $ python makeignore.py
18 18
19 19 Should display baz only:
20 20
21 21 $ hg status
22 22 ? baz
23 23
24 24 $ rm foo bar baz .hgignore makeignore.py
25 25
26 26 $ touch a.o
27 27 $ touch a.c
28 28 $ touch syntax
29 29 $ mkdir dir
30 30 $ touch dir/a.o
31 31 $ touch dir/b.o
32 32 $ touch dir/c.o
33 33
34 34 $ hg add dir/a.o
35 35 $ hg commit -m 0
36 36 $ hg add dir/b.o
37 37
38 38 $ hg status
39 39 A dir/b.o
40 40 ? a.c
41 41 ? a.o
42 42 ? dir/c.o
43 43 ? syntax
44 44
45 45 $ echo "*.o" > .hgignore
46 46 $ hg status
47 47 abort: $TESTTMP/.hgignore: invalid pattern (relre): *.o (glob)
48 48 [255]
49 49
50 50 $ echo ".*\.o" > .hgignore
51 51 $ hg status
52 52 A dir/b.o
53 53 ? .hgignore
54 54 ? a.c
55 55 ? syntax
56 56
57 57 Check it does not ignore the current directory '.':
58 58
59 59 $ echo "^\." > .hgignore
60 60 $ hg status
61 61 A dir/b.o
62 62 ? a.c
63 63 ? a.o
64 64 ? dir/c.o
65 65 ? syntax
66 66
67 67 $ echo "glob:**.o" > .hgignore
68 68 $ hg status
69 69 A dir/b.o
70 70 ? .hgignore
71 71 ? a.c
72 72 ? syntax
73 73
74 74 $ echo "glob:*.o" > .hgignore
75 75 $ hg status
76 76 A dir/b.o
77 77 ? .hgignore
78 78 ? a.c
79 79 ? syntax
80 80
81 81 $ echo "syntax: glob" > .hgignore
82 82 $ echo "re:.*\.o" >> .hgignore
83 83 $ hg status
84 84 A dir/b.o
85 85 ? .hgignore
86 86 ? a.c
87 87 ? syntax
88 88
89 89 $ echo "syntax: invalid" > .hgignore
90 90 $ hg status
91 91 $TESTTMP/.hgignore: ignoring invalid syntax 'invalid' (glob)
92 92 A dir/b.o
93 93 ? .hgignore
94 94 ? a.c
95 95 ? a.o
96 96 ? dir/c.o
97 97 ? syntax
98 98
99 99 $ echo "syntax: glob" > .hgignore
100 100 $ echo "*.o" >> .hgignore
101 101 $ hg status
102 102 A dir/b.o
103 103 ? .hgignore
104 104 ? a.c
105 105 ? syntax
106 106
107 107 $ echo "relglob:syntax*" > .hgignore
108 108 $ hg status
109 109 A dir/b.o
110 110 ? .hgignore
111 111 ? a.c
112 112 ? a.o
113 113 ? dir/c.o
114 114
115 115 $ echo "relglob:*" > .hgignore
116 116 $ hg status
117 117 A dir/b.o
118 118
119 119 $ cd dir
120 120 $ hg status .
121 121 A b.o
122 122
123 123 $ hg debugignore
124 124 (?:(?:|.*/)[^/]*(?:/|$))
125 125
126 126 $ cd ..
127
128 Check patterns that match only the directory
129
130 $ echo "^dir\$" > .hgignore
131 $ hg status
132 A dir/b.o
133 ? .hgignore
134 ? a.c
135 ? a.o
136 ? syntax
@@ -1,278 +1,302 b''
1 1 $ hg init repo
2 2 $ cd repo
3 3 $ echo 123 > a
4 4 $ echo 123 > c
5 5 $ echo 123 > e
6 6 $ hg add a c e
7 7 $ hg commit -m "first" a c e
8 8
9 9 nothing changed
10 10
11 11 $ hg revert
12 12 abort: no files or directories specified
13 13 (use --all to revert all files)
14 14 [255]
15 15 $ hg revert --all
16 16
17 17 $ echo 123 > b
18 18
19 19 should show b unknown
20 20
21 21 $ hg status
22 22 ? b
23 23 $ echo 12 > c
24 24
25 25 should show b unknown and c modified
26 26
27 27 $ hg status
28 28 M c
29 29 ? b
30 30 $ hg add b
31 31
32 32 should show b added and c modified
33 33
34 34 $ hg status
35 35 M c
36 36 A b
37 37 $ hg rm a
38 38
39 39 should show a removed, b added and c modified
40 40
41 41 $ hg status
42 42 M c
43 43 A b
44 44 R a
45 45 $ hg revert a
46 46
47 47 should show b added, copy saved, and c modified
48 48
49 49 $ hg status
50 50 M c
51 51 A b
52 52 $ hg revert b
53 53
54 54 should show b unknown, and c modified
55 55
56 56 $ hg status
57 57 M c
58 58 ? b
59 59 $ hg revert --no-backup c
60 60
61 61 should show unknown: b
62 62
63 63 $ hg status
64 64 ? b
65 65 $ hg add b
66 66
67 67 should show b added
68 68
69 69 $ hg status b
70 70 A b
71 71 $ rm b
72 72
73 73 should show b deleted
74 74
75 75 $ hg status b
76 76 ! b
77 77 $ hg revert -v b
78 78 forgetting b
79 79
80 80 should not find b
81 81
82 82 $ hg status b
83 83 b: * (glob)
84 84
85 85 should show a c e
86 86
87 87 $ ls
88 88 a
89 89 c
90 90 e
91 91
92 92 should verbosely save backup to e.orig
93 93
94 94 $ echo z > e
95 95 $ hg revert --all -v
96 96 saving current version of e as e.orig
97 97 reverting e
98 98
99 99 should say no changes needed
100 100
101 101 $ hg revert a
102 102 no changes needed to a
103 103
104 104 should say file not managed
105 105
106 106 $ echo q > q
107 107 $ hg revert q
108 108 file not managed: q
109 109 $ rm q
110 110
111 111 should say file not found
112 112
113 113 $ hg revert notfound
114 114 notfound: no such file in rev 334a9e57682c
115 115 $ touch d
116 116 $ hg add d
117 117 $ hg rm a
118 118 $ hg commit -m "second"
119 119 $ echo z > z
120 120 $ hg add z
121 121 $ hg st
122 122 A z
123 123 ? e.orig
124 124
125 125 should add a, remove d, forget z
126 126
127 127 $ hg revert --all -r0
128 128 adding a
129 129 removing d
130 130 forgetting z
131 131
132 132 should forget a, undelete d
133 133
134 134 $ hg revert --all -rtip
135 135 forgetting a
136 136 undeleting d
137 137 $ rm a *.orig
138 138
139 139 should silently add a
140 140
141 141 $ hg revert -r0 a
142 142 $ hg st a
143 143 A a
144 144 $ hg rm d
145 145 $ hg st d
146 146 R d
147 147
148 148 should silently keep d removed
149 149
150 150 $ hg revert -r0 d
151 151 $ hg st d
152 152 R d
153 153
154 154 $ hg update -C
155 155 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
156 156 #if execbit
157 157 $ chmod +x c
158 158 $ hg revert --all
159 159 reverting c
160 160
161 161 should print non-executable
162 162
163 163 $ test -x c || echo non-executable
164 164 non-executable
165 165
166 166 $ chmod +x c
167 167 $ hg commit -m exe
168 168
169 169 $ chmod -x c
170 170 $ hg revert --all
171 171 reverting c
172 172
173 173 should print executable
174 174
175 175 $ test -x c && echo executable
176 176 executable
177 177 #endif
178 178
179 179 $ cd ..
180 180
181 181
182 182 Issue241: update and revert produces inconsistent repositories
183 183
184 184 $ hg init a
185 185 $ cd a
186 186 $ echo a >> a
187 187 $ hg commit -A -d '1 0' -m a
188 188 adding a
189 189 $ echo a >> a
190 190 $ hg commit -d '2 0' -m a
191 191 $ hg update 0
192 192 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
193 193 $ mkdir b
194 194 $ echo b > b/b
195 195
196 196 should fail - no arguments
197 197
198 198 $ hg revert -rtip
199 199 abort: no files or directories specified
200 200 (use --all to revert all files, or 'hg update 1' to update)
201 201 [255]
202 202
203 203 should succeed
204 204
205 205 $ hg revert --all -rtip
206 206 reverting a
207 207
208 208
209 209 Issue332: confusing message when reverting directory
210 210
211 211 $ hg ci -A -m b
212 212 adding b/b
213 213 created new head
214 214 $ echo foobar > b/b
215 215 $ mkdir newdir
216 216 $ echo foo > newdir/newfile
217 217 $ hg add newdir/newfile
218 218 $ hg revert b newdir
219 219 reverting b/b (glob)
220 220 forgetting newdir/newfile (glob)
221 221 $ echo foobar > b/b
222 222 $ hg revert .
223 223 reverting b/b (glob)
224 224
225 225
226 226 reverting a rename target should revert the source
227 227
228 228 $ hg mv a newa
229 229 $ hg revert newa
230 230 $ hg st a newa
231 231 ? newa
232 232
233 233 $ cd ..
234 234
235 235 $ hg init ignored
236 236 $ cd ignored
237 237 $ echo '^ignored$' > .hgignore
238 238 $ echo '^ignoreddir$' >> .hgignore
239 239 $ echo '^removed$' >> .hgignore
240 240
241 241 $ mkdir ignoreddir
242 242 $ touch ignoreddir/file
243 243 $ touch ignoreddir/removed
244 244 $ touch ignored
245 245 $ touch removed
246 246
247 247 4 ignored files (we will add/commit everything)
248 248
249 249 $ hg st -A -X .hgignore
250 250 I ignored
251 251 I ignoreddir/file
252 252 I ignoreddir/removed
253 253 I removed
254 254 $ hg ci -qAm 'add files' ignored ignoreddir/file ignoreddir/removed removed
255 255
256 256 $ echo >> ignored
257 257 $ echo >> ignoreddir/file
258 258 $ hg rm removed ignoreddir/removed
259 259
260 260 should revert ignored* and undelete *removed
261 261
262 262 $ hg revert -a --no-backup
263 263 reverting ignored
264 264 reverting ignoreddir/file (glob)
265 265 undeleting ignoreddir/removed (glob)
266 266 undeleting removed
267 267 $ hg st -mardi
268 268
269 269 $ hg up -qC
270 270 $ echo >> ignored
271 271 $ hg rm removed
272 272
273 273 should silently revert the named files
274 274
275 275 $ hg revert --no-backup ignored removed
276 276 $ hg st -mardi
277 277
278 someone set up us the copies
279
280 $ rm .hgignore
281 $ hg update -C
282 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
283 $ hg mv ignored allyour
284 $ hg copy removed base
285 $ hg commit -m rename
286
287 copies and renames, you have no chance to survive make your time (issue3920)
288
289 $ hg update '.^'
290 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
291 $ hg revert -rtip -a
292 adding allyour
293 adding base
294 removing ignored
295 $ hg status -C
296 A allyour
297 ignored
298 A base
299 removed
300 R ignored
301
278 302 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now