##// END OF EJS Templates
scmutil: fold in wdutil
Matt Mackall -
r14320:3438417a default
parent child Browse files
Show More
@@ -1,1195 +1,1195
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 import util, scmutil, templater, patch, error, templatekw, wdutil
11 import util, scmutil, templater, patch, error, templatekw
12 12 import match as matchmod
13 13 import subrepo
14 14
15 expandpats = wdutil.expandpats
16 match = wdutil.match
17 matchall = wdutil.matchall
18 matchfiles = wdutil.matchfiles
19 addremove = wdutil.addremove
20 dirstatecopy = wdutil.dirstatecopy
15 expandpats = scmutil.expandpats
16 match = scmutil.match
17 matchall = scmutil.matchall
18 matchfiles = scmutil.matchfiles
19 addremove = scmutil.addremove
20 dirstatecopy = scmutil.dirstatecopy
21 21
22 22 def parsealiases(cmd):
23 23 return cmd.lstrip("^").split("|")
24 24
25 25 def findpossible(cmd, table, strict=False):
26 26 """
27 27 Return cmd -> (aliases, command table entry)
28 28 for each matching command.
29 29 Return debug commands (or their aliases) only if no normal command matches.
30 30 """
31 31 choice = {}
32 32 debugchoice = {}
33 33 for e in table.keys():
34 34 aliases = parsealiases(e)
35 35 found = None
36 36 if cmd in aliases:
37 37 found = cmd
38 38 elif not strict:
39 39 for a in aliases:
40 40 if a.startswith(cmd):
41 41 found = a
42 42 break
43 43 if found is not None:
44 44 if aliases[0].startswith("debug") or found.startswith("debug"):
45 45 debugchoice[found] = (aliases, table[e])
46 46 else:
47 47 choice[found] = (aliases, table[e])
48 48
49 49 if not choice and debugchoice:
50 50 choice = debugchoice
51 51
52 52 return choice
53 53
54 54 def findcmd(cmd, table, strict=True):
55 55 """Return (aliases, command table entry) for command string."""
56 56 choice = findpossible(cmd, table, strict)
57 57
58 58 if cmd in choice:
59 59 return choice[cmd]
60 60
61 61 if len(choice) > 1:
62 62 clist = choice.keys()
63 63 clist.sort()
64 64 raise error.AmbiguousCommand(cmd, clist)
65 65
66 66 if choice:
67 67 return choice.values()[0]
68 68
69 69 raise error.UnknownCommand(cmd)
70 70
71 71 def findrepo(p):
72 72 while not os.path.isdir(os.path.join(p, ".hg")):
73 73 oldp, p = p, os.path.dirname(p)
74 74 if p == oldp:
75 75 return None
76 76
77 77 return p
78 78
79 79 def bailifchanged(repo):
80 80 if repo.dirstate.p2() != nullid:
81 81 raise util.Abort(_('outstanding uncommitted merge'))
82 82 modified, added, removed, deleted = repo.status()[:4]
83 83 if modified or added or removed or deleted:
84 84 raise util.Abort(_("outstanding uncommitted changes"))
85 85
86 86 def logmessage(opts):
87 87 """ get the log message according to -m and -l option """
88 88 message = opts.get('message')
89 89 logfile = opts.get('logfile')
90 90
91 91 if message and logfile:
92 92 raise util.Abort(_('options --message and --logfile are mutually '
93 93 'exclusive'))
94 94 if not message and logfile:
95 95 try:
96 96 if logfile == '-':
97 97 message = sys.stdin.read()
98 98 else:
99 99 message = '\n'.join(util.readfile(logfile).splitlines())
100 100 except IOError, inst:
101 101 raise util.Abort(_("can't read commit message '%s': %s") %
102 102 (logfile, inst.strerror))
103 103 return message
104 104
105 105 def loglimit(opts):
106 106 """get the log limit according to option -l/--limit"""
107 107 limit = opts.get('limit')
108 108 if limit:
109 109 try:
110 110 limit = int(limit)
111 111 except ValueError:
112 112 raise util.Abort(_('limit must be a positive integer'))
113 113 if limit <= 0:
114 114 raise util.Abort(_('limit must be positive'))
115 115 else:
116 116 limit = None
117 117 return limit
118 118
119 119 def makefilename(repo, pat, node,
120 120 total=None, seqno=None, revwidth=None, pathname=None):
121 121 node_expander = {
122 122 'H': lambda: hex(node),
123 123 'R': lambda: str(repo.changelog.rev(node)),
124 124 'h': lambda: short(node),
125 125 }
126 126 expander = {
127 127 '%': lambda: '%',
128 128 'b': lambda: os.path.basename(repo.root),
129 129 }
130 130
131 131 try:
132 132 if node:
133 133 expander.update(node_expander)
134 134 if node:
135 135 expander['r'] = (lambda:
136 136 str(repo.changelog.rev(node)).zfill(revwidth or 0))
137 137 if total is not None:
138 138 expander['N'] = lambda: str(total)
139 139 if seqno is not None:
140 140 expander['n'] = lambda: str(seqno)
141 141 if total is not None and seqno is not None:
142 142 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
143 143 if pathname is not None:
144 144 expander['s'] = lambda: os.path.basename(pathname)
145 145 expander['d'] = lambda: os.path.dirname(pathname) or '.'
146 146 expander['p'] = lambda: pathname
147 147
148 148 newname = []
149 149 patlen = len(pat)
150 150 i = 0
151 151 while i < patlen:
152 152 c = pat[i]
153 153 if c == '%':
154 154 i += 1
155 155 c = pat[i]
156 156 c = expander[c]()
157 157 newname.append(c)
158 158 i += 1
159 159 return ''.join(newname)
160 160 except KeyError, inst:
161 161 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
162 162 inst.args[0])
163 163
164 164 def makefileobj(repo, pat, node=None, total=None,
165 165 seqno=None, revwidth=None, mode='wb', pathname=None):
166 166
167 167 writable = mode not in ('r', 'rb')
168 168
169 169 if not pat or pat == '-':
170 170 fp = writable and sys.stdout or sys.stdin
171 171 return os.fdopen(os.dup(fp.fileno()), mode)
172 172 if hasattr(pat, 'write') and writable:
173 173 return pat
174 174 if hasattr(pat, 'read') and 'r' in mode:
175 175 return pat
176 176 return open(makefilename(repo, pat, node, total, seqno, revwidth,
177 177 pathname),
178 178 mode)
179 179
180 180 def copy(ui, repo, pats, opts, rename=False):
181 181 # called with the repo lock held
182 182 #
183 183 # hgsep => pathname that uses "/" to separate directories
184 184 # ossep => pathname that uses os.sep to separate directories
185 185 cwd = repo.getcwd()
186 186 targets = {}
187 187 after = opts.get("after")
188 188 dryrun = opts.get("dry_run")
189 189 wctx = repo[None]
190 190
191 191 def walkpat(pat):
192 192 srcs = []
193 193 badstates = after and '?' or '?r'
194 194 m = match(repo, [pat], opts, globbed=True)
195 195 for abs in repo.walk(m):
196 196 state = repo.dirstate[abs]
197 197 rel = m.rel(abs)
198 198 exact = m.exact(abs)
199 199 if state in badstates:
200 200 if exact and state == '?':
201 201 ui.warn(_('%s: not copying - file is not managed\n') % rel)
202 202 if exact and state == 'r':
203 203 ui.warn(_('%s: not copying - file has been marked for'
204 204 ' remove\n') % rel)
205 205 continue
206 206 # abs: hgsep
207 207 # rel: ossep
208 208 srcs.append((abs, rel, exact))
209 209 return srcs
210 210
211 211 # abssrc: hgsep
212 212 # relsrc: ossep
213 213 # otarget: ossep
214 214 def copyfile(abssrc, relsrc, otarget, exact):
215 215 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
216 216 reltarget = repo.pathto(abstarget, cwd)
217 217 target = repo.wjoin(abstarget)
218 218 src = repo.wjoin(abssrc)
219 219 state = repo.dirstate[abstarget]
220 220
221 221 scmutil.checkportable(ui, abstarget)
222 222
223 223 # check for collisions
224 224 prevsrc = targets.get(abstarget)
225 225 if prevsrc is not None:
226 226 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
227 227 (reltarget, repo.pathto(abssrc, cwd),
228 228 repo.pathto(prevsrc, cwd)))
229 229 return
230 230
231 231 # check for overwrites
232 232 exists = os.path.lexists(target)
233 233 if not after and exists or after and state in 'mn':
234 234 if not opts['force']:
235 235 ui.warn(_('%s: not overwriting - file exists\n') %
236 236 reltarget)
237 237 return
238 238
239 239 if after:
240 240 if not exists:
241 241 if rename:
242 242 ui.warn(_('%s: not recording move - %s does not exist\n') %
243 243 (relsrc, reltarget))
244 244 else:
245 245 ui.warn(_('%s: not recording copy - %s does not exist\n') %
246 246 (relsrc, reltarget))
247 247 return
248 248 elif not dryrun:
249 249 try:
250 250 if exists:
251 251 os.unlink(target)
252 252 targetdir = os.path.dirname(target) or '.'
253 253 if not os.path.isdir(targetdir):
254 254 os.makedirs(targetdir)
255 255 util.copyfile(src, target)
256 256 except IOError, inst:
257 257 if inst.errno == errno.ENOENT:
258 258 ui.warn(_('%s: deleted in working copy\n') % relsrc)
259 259 else:
260 260 ui.warn(_('%s: cannot copy - %s\n') %
261 261 (relsrc, inst.strerror))
262 262 return True # report a failure
263 263
264 264 if ui.verbose or not exact:
265 265 if rename:
266 266 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
267 267 else:
268 268 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
269 269
270 270 targets[abstarget] = abssrc
271 271
272 272 # fix up dirstate
273 273 dirstatecopy(ui, repo, wctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd)
274 274 if rename and not dryrun:
275 275 wctx.remove([abssrc], not after)
276 276
277 277 # pat: ossep
278 278 # dest ossep
279 279 # srcs: list of (hgsep, hgsep, ossep, bool)
280 280 # return: function that takes hgsep and returns ossep
281 281 def targetpathfn(pat, dest, srcs):
282 282 if os.path.isdir(pat):
283 283 abspfx = scmutil.canonpath(repo.root, cwd, pat)
284 284 abspfx = util.localpath(abspfx)
285 285 if destdirexists:
286 286 striplen = len(os.path.split(abspfx)[0])
287 287 else:
288 288 striplen = len(abspfx)
289 289 if striplen:
290 290 striplen += len(os.sep)
291 291 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
292 292 elif destdirexists:
293 293 res = lambda p: os.path.join(dest,
294 294 os.path.basename(util.localpath(p)))
295 295 else:
296 296 res = lambda p: dest
297 297 return res
298 298
299 299 # pat: ossep
300 300 # dest ossep
301 301 # srcs: list of (hgsep, hgsep, ossep, bool)
302 302 # return: function that takes hgsep and returns ossep
303 303 def targetpathafterfn(pat, dest, srcs):
304 304 if matchmod.patkind(pat):
305 305 # a mercurial pattern
306 306 res = lambda p: os.path.join(dest,
307 307 os.path.basename(util.localpath(p)))
308 308 else:
309 309 abspfx = scmutil.canonpath(repo.root, cwd, pat)
310 310 if len(abspfx) < len(srcs[0][0]):
311 311 # A directory. Either the target path contains the last
312 312 # component of the source path or it does not.
313 313 def evalpath(striplen):
314 314 score = 0
315 315 for s in srcs:
316 316 t = os.path.join(dest, util.localpath(s[0])[striplen:])
317 317 if os.path.lexists(t):
318 318 score += 1
319 319 return score
320 320
321 321 abspfx = util.localpath(abspfx)
322 322 striplen = len(abspfx)
323 323 if striplen:
324 324 striplen += len(os.sep)
325 325 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
326 326 score = evalpath(striplen)
327 327 striplen1 = len(os.path.split(abspfx)[0])
328 328 if striplen1:
329 329 striplen1 += len(os.sep)
330 330 if evalpath(striplen1) > score:
331 331 striplen = striplen1
332 332 res = lambda p: os.path.join(dest,
333 333 util.localpath(p)[striplen:])
334 334 else:
335 335 # a file
336 336 if destdirexists:
337 337 res = lambda p: os.path.join(dest,
338 338 os.path.basename(util.localpath(p)))
339 339 else:
340 340 res = lambda p: dest
341 341 return res
342 342
343 343
344 344 pats = expandpats(pats)
345 345 if not pats:
346 346 raise util.Abort(_('no source or destination specified'))
347 347 if len(pats) == 1:
348 348 raise util.Abort(_('no destination specified'))
349 349 dest = pats.pop()
350 350 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
351 351 if not destdirexists:
352 352 if len(pats) > 1 or matchmod.patkind(pats[0]):
353 353 raise util.Abort(_('with multiple sources, destination must be an '
354 354 'existing directory'))
355 355 if util.endswithsep(dest):
356 356 raise util.Abort(_('destination %s is not a directory') % dest)
357 357
358 358 tfn = targetpathfn
359 359 if after:
360 360 tfn = targetpathafterfn
361 361 copylist = []
362 362 for pat in pats:
363 363 srcs = walkpat(pat)
364 364 if not srcs:
365 365 continue
366 366 copylist.append((tfn(pat, dest, srcs), srcs))
367 367 if not copylist:
368 368 raise util.Abort(_('no files to copy'))
369 369
370 370 errors = 0
371 371 for targetpath, srcs in copylist:
372 372 for abssrc, relsrc, exact in srcs:
373 373 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
374 374 errors += 1
375 375
376 376 if errors:
377 377 ui.warn(_('(consider using --after)\n'))
378 378
379 379 return errors != 0
380 380
381 381 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
382 382 runargs=None, appendpid=False):
383 383 '''Run a command as a service.'''
384 384
385 385 if opts['daemon'] and not opts['daemon_pipefds']:
386 386 # Signal child process startup with file removal
387 387 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
388 388 os.close(lockfd)
389 389 try:
390 390 if not runargs:
391 391 runargs = util.hgcmd() + sys.argv[1:]
392 392 runargs.append('--daemon-pipefds=%s' % lockpath)
393 393 # Don't pass --cwd to the child process, because we've already
394 394 # changed directory.
395 395 for i in xrange(1, len(runargs)):
396 396 if runargs[i].startswith('--cwd='):
397 397 del runargs[i]
398 398 break
399 399 elif runargs[i].startswith('--cwd'):
400 400 del runargs[i:i + 2]
401 401 break
402 402 def condfn():
403 403 return not os.path.exists(lockpath)
404 404 pid = util.rundetached(runargs, condfn)
405 405 if pid < 0:
406 406 raise util.Abort(_('child process failed to start'))
407 407 finally:
408 408 try:
409 409 os.unlink(lockpath)
410 410 except OSError, e:
411 411 if e.errno != errno.ENOENT:
412 412 raise
413 413 if parentfn:
414 414 return parentfn(pid)
415 415 else:
416 416 return
417 417
418 418 if initfn:
419 419 initfn()
420 420
421 421 if opts['pid_file']:
422 422 mode = appendpid and 'a' or 'w'
423 423 fp = open(opts['pid_file'], mode)
424 424 fp.write(str(os.getpid()) + '\n')
425 425 fp.close()
426 426
427 427 if opts['daemon_pipefds']:
428 428 lockpath = opts['daemon_pipefds']
429 429 try:
430 430 os.setsid()
431 431 except AttributeError:
432 432 pass
433 433 os.unlink(lockpath)
434 434 util.hidewindow()
435 435 sys.stdout.flush()
436 436 sys.stderr.flush()
437 437
438 438 nullfd = os.open(util.nulldev, os.O_RDWR)
439 439 logfilefd = nullfd
440 440 if logfile:
441 441 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
442 442 os.dup2(nullfd, 0)
443 443 os.dup2(logfilefd, 1)
444 444 os.dup2(logfilefd, 2)
445 445 if nullfd not in (0, 1, 2):
446 446 os.close(nullfd)
447 447 if logfile and logfilefd not in (0, 1, 2):
448 448 os.close(logfilefd)
449 449
450 450 if runfn:
451 451 return runfn()
452 452
453 453 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
454 454 opts=None):
455 455 '''export changesets as hg patches.'''
456 456
457 457 total = len(revs)
458 458 revwidth = max([len(str(rev)) for rev in revs])
459 459
460 460 def single(rev, seqno, fp):
461 461 ctx = repo[rev]
462 462 node = ctx.node()
463 463 parents = [p.node() for p in ctx.parents() if p]
464 464 branch = ctx.branch()
465 465 if switch_parent:
466 466 parents.reverse()
467 467 prev = (parents and parents[0]) or nullid
468 468
469 469 shouldclose = False
470 470 if not fp:
471 471 fp = makefileobj(repo, template, node, total=total, seqno=seqno,
472 472 revwidth=revwidth, mode='ab')
473 473 if fp != template:
474 474 shouldclose = True
475 475 if fp != sys.stdout and hasattr(fp, 'name'):
476 476 repo.ui.note("%s\n" % fp.name)
477 477
478 478 fp.write("# HG changeset patch\n")
479 479 fp.write("# User %s\n" % ctx.user())
480 480 fp.write("# Date %d %d\n" % ctx.date())
481 481 if branch and branch != 'default':
482 482 fp.write("# Branch %s\n" % branch)
483 483 fp.write("# Node ID %s\n" % hex(node))
484 484 fp.write("# Parent %s\n" % hex(prev))
485 485 if len(parents) > 1:
486 486 fp.write("# Parent %s\n" % hex(parents[1]))
487 487 fp.write(ctx.description().rstrip())
488 488 fp.write("\n\n")
489 489
490 490 for chunk in patch.diff(repo, prev, node, opts=opts):
491 491 fp.write(chunk)
492 492
493 493 if shouldclose:
494 494 fp.close()
495 495
496 496 for seqno, rev in enumerate(revs):
497 497 single(rev, seqno + 1, fp)
498 498
499 499 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
500 500 changes=None, stat=False, fp=None, prefix='',
501 501 listsubrepos=False):
502 502 '''show diff or diffstat.'''
503 503 if fp is None:
504 504 write = ui.write
505 505 else:
506 506 def write(s, **kw):
507 507 fp.write(s)
508 508
509 509 if stat:
510 510 diffopts = diffopts.copy(context=0)
511 511 width = 80
512 512 if not ui.plain():
513 513 width = ui.termwidth()
514 514 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
515 515 prefix=prefix)
516 516 for chunk, label in patch.diffstatui(util.iterlines(chunks),
517 517 width=width,
518 518 git=diffopts.git):
519 519 write(chunk, label=label)
520 520 else:
521 521 for chunk, label in patch.diffui(repo, node1, node2, match,
522 522 changes, diffopts, prefix=prefix):
523 523 write(chunk, label=label)
524 524
525 525 if listsubrepos:
526 526 ctx1 = repo[node1]
527 527 ctx2 = repo[node2]
528 528 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
529 529 if node2 is not None:
530 530 node2 = ctx2.substate[subpath][1]
531 531 submatch = matchmod.narrowmatcher(subpath, match)
532 532 sub.diff(diffopts, node2, submatch, changes=changes,
533 533 stat=stat, fp=fp, prefix=prefix)
534 534
535 535 class changeset_printer(object):
536 536 '''show changeset information when templating not requested.'''
537 537
538 538 def __init__(self, ui, repo, patch, diffopts, buffered):
539 539 self.ui = ui
540 540 self.repo = repo
541 541 self.buffered = buffered
542 542 self.patch = patch
543 543 self.diffopts = diffopts
544 544 self.header = {}
545 545 self.hunk = {}
546 546 self.lastheader = None
547 547 self.footer = None
548 548
549 549 def flush(self, rev):
550 550 if rev in self.header:
551 551 h = self.header[rev]
552 552 if h != self.lastheader:
553 553 self.lastheader = h
554 554 self.ui.write(h)
555 555 del self.header[rev]
556 556 if rev in self.hunk:
557 557 self.ui.write(self.hunk[rev])
558 558 del self.hunk[rev]
559 559 return 1
560 560 return 0
561 561
562 562 def close(self):
563 563 if self.footer:
564 564 self.ui.write(self.footer)
565 565
566 566 def show(self, ctx, copies=None, matchfn=None, **props):
567 567 if self.buffered:
568 568 self.ui.pushbuffer()
569 569 self._show(ctx, copies, matchfn, props)
570 570 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
571 571 else:
572 572 self._show(ctx, copies, matchfn, props)
573 573
574 574 def _show(self, ctx, copies, matchfn, props):
575 575 '''show a single changeset or file revision'''
576 576 changenode = ctx.node()
577 577 rev = ctx.rev()
578 578
579 579 if self.ui.quiet:
580 580 self.ui.write("%d:%s\n" % (rev, short(changenode)),
581 581 label='log.node')
582 582 return
583 583
584 584 log = self.repo.changelog
585 585 date = util.datestr(ctx.date())
586 586
587 587 hexfunc = self.ui.debugflag and hex or short
588 588
589 589 parents = [(p, hexfunc(log.node(p)))
590 590 for p in self._meaningful_parentrevs(log, rev)]
591 591
592 592 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
593 593 label='log.changeset')
594 594
595 595 branch = ctx.branch()
596 596 # don't show the default branch name
597 597 if branch != 'default':
598 598 self.ui.write(_("branch: %s\n") % branch,
599 599 label='log.branch')
600 600 for bookmark in self.repo.nodebookmarks(changenode):
601 601 self.ui.write(_("bookmark: %s\n") % bookmark,
602 602 label='log.bookmark')
603 603 for tag in self.repo.nodetags(changenode):
604 604 self.ui.write(_("tag: %s\n") % tag,
605 605 label='log.tag')
606 606 for parent in parents:
607 607 self.ui.write(_("parent: %d:%s\n") % parent,
608 608 label='log.parent')
609 609
610 610 if self.ui.debugflag:
611 611 mnode = ctx.manifestnode()
612 612 self.ui.write(_("manifest: %d:%s\n") %
613 613 (self.repo.manifest.rev(mnode), hex(mnode)),
614 614 label='ui.debug log.manifest')
615 615 self.ui.write(_("user: %s\n") % ctx.user(),
616 616 label='log.user')
617 617 self.ui.write(_("date: %s\n") % date,
618 618 label='log.date')
619 619
620 620 if self.ui.debugflag:
621 621 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
622 622 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
623 623 files):
624 624 if value:
625 625 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
626 626 label='ui.debug log.files')
627 627 elif ctx.files() and self.ui.verbose:
628 628 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
629 629 label='ui.note log.files')
630 630 if copies and self.ui.verbose:
631 631 copies = ['%s (%s)' % c for c in copies]
632 632 self.ui.write(_("copies: %s\n") % ' '.join(copies),
633 633 label='ui.note log.copies')
634 634
635 635 extra = ctx.extra()
636 636 if extra and self.ui.debugflag:
637 637 for key, value in sorted(extra.items()):
638 638 self.ui.write(_("extra: %s=%s\n")
639 639 % (key, value.encode('string_escape')),
640 640 label='ui.debug log.extra')
641 641
642 642 description = ctx.description().strip()
643 643 if description:
644 644 if self.ui.verbose:
645 645 self.ui.write(_("description:\n"),
646 646 label='ui.note log.description')
647 647 self.ui.write(description,
648 648 label='ui.note log.description')
649 649 self.ui.write("\n\n")
650 650 else:
651 651 self.ui.write(_("summary: %s\n") %
652 652 description.splitlines()[0],
653 653 label='log.summary')
654 654 self.ui.write("\n")
655 655
656 656 self.showpatch(changenode, matchfn)
657 657
658 658 def showpatch(self, node, matchfn):
659 659 if not matchfn:
660 660 matchfn = self.patch
661 661 if matchfn:
662 662 stat = self.diffopts.get('stat')
663 663 diff = self.diffopts.get('patch')
664 664 diffopts = patch.diffopts(self.ui, self.diffopts)
665 665 prev = self.repo.changelog.parents(node)[0]
666 666 if stat:
667 667 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
668 668 match=matchfn, stat=True)
669 669 if diff:
670 670 if stat:
671 671 self.ui.write("\n")
672 672 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
673 673 match=matchfn, stat=False)
674 674 self.ui.write("\n")
675 675
676 676 def _meaningful_parentrevs(self, log, rev):
677 677 """Return list of meaningful (or all if debug) parentrevs for rev.
678 678
679 679 For merges (two non-nullrev revisions) both parents are meaningful.
680 680 Otherwise the first parent revision is considered meaningful if it
681 681 is not the preceding revision.
682 682 """
683 683 parents = log.parentrevs(rev)
684 684 if not self.ui.debugflag and parents[1] == nullrev:
685 685 if parents[0] >= rev - 1:
686 686 parents = []
687 687 else:
688 688 parents = [parents[0]]
689 689 return parents
690 690
691 691
692 692 class changeset_templater(changeset_printer):
693 693 '''format changeset information.'''
694 694
695 695 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
696 696 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
697 697 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
698 698 defaulttempl = {
699 699 'parent': '{rev}:{node|formatnode} ',
700 700 'manifest': '{rev}:{node|formatnode}',
701 701 'file_copy': '{name} ({source})',
702 702 'extra': '{key}={value|stringescape}'
703 703 }
704 704 # filecopy is preserved for compatibility reasons
705 705 defaulttempl['filecopy'] = defaulttempl['file_copy']
706 706 self.t = templater.templater(mapfile, {'formatnode': formatnode},
707 707 cache=defaulttempl)
708 708 self.cache = {}
709 709
710 710 def use_template(self, t):
711 711 '''set template string to use'''
712 712 self.t.cache['changeset'] = t
713 713
714 714 def _meaningful_parentrevs(self, ctx):
715 715 """Return list of meaningful (or all if debug) parentrevs for rev.
716 716 """
717 717 parents = ctx.parents()
718 718 if len(parents) > 1:
719 719 return parents
720 720 if self.ui.debugflag:
721 721 return [parents[0], self.repo['null']]
722 722 if parents[0].rev() >= ctx.rev() - 1:
723 723 return []
724 724 return parents
725 725
726 726 def _show(self, ctx, copies, matchfn, props):
727 727 '''show a single changeset or file revision'''
728 728
729 729 showlist = templatekw.showlist
730 730
731 731 # showparents() behaviour depends on ui trace level which
732 732 # causes unexpected behaviours at templating level and makes
733 733 # it harder to extract it in a standalone function. Its
734 734 # behaviour cannot be changed so leave it here for now.
735 735 def showparents(**args):
736 736 ctx = args['ctx']
737 737 parents = [[('rev', p.rev()), ('node', p.hex())]
738 738 for p in self._meaningful_parentrevs(ctx)]
739 739 return showlist('parent', parents, **args)
740 740
741 741 props = props.copy()
742 742 props.update(templatekw.keywords)
743 743 props['parents'] = showparents
744 744 props['templ'] = self.t
745 745 props['ctx'] = ctx
746 746 props['repo'] = self.repo
747 747 props['revcache'] = {'copies': copies}
748 748 props['cache'] = self.cache
749 749
750 750 # find correct templates for current mode
751 751
752 752 tmplmodes = [
753 753 (True, None),
754 754 (self.ui.verbose, 'verbose'),
755 755 (self.ui.quiet, 'quiet'),
756 756 (self.ui.debugflag, 'debug'),
757 757 ]
758 758
759 759 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
760 760 for mode, postfix in tmplmodes:
761 761 for type in types:
762 762 cur = postfix and ('%s_%s' % (type, postfix)) or type
763 763 if mode and cur in self.t:
764 764 types[type] = cur
765 765
766 766 try:
767 767
768 768 # write header
769 769 if types['header']:
770 770 h = templater.stringify(self.t(types['header'], **props))
771 771 if self.buffered:
772 772 self.header[ctx.rev()] = h
773 773 else:
774 774 if self.lastheader != h:
775 775 self.lastheader = h
776 776 self.ui.write(h)
777 777
778 778 # write changeset metadata, then patch if requested
779 779 key = types['changeset']
780 780 self.ui.write(templater.stringify(self.t(key, **props)))
781 781 self.showpatch(ctx.node(), matchfn)
782 782
783 783 if types['footer']:
784 784 if not self.footer:
785 785 self.footer = templater.stringify(self.t(types['footer'],
786 786 **props))
787 787
788 788 except KeyError, inst:
789 789 msg = _("%s: no key named '%s'")
790 790 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
791 791 except SyntaxError, inst:
792 792 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
793 793
794 794 def show_changeset(ui, repo, opts, buffered=False):
795 795 """show one changeset using template or regular display.
796 796
797 797 Display format will be the first non-empty hit of:
798 798 1. option 'template'
799 799 2. option 'style'
800 800 3. [ui] setting 'logtemplate'
801 801 4. [ui] setting 'style'
802 802 If all of these values are either the unset or the empty string,
803 803 regular display via changeset_printer() is done.
804 804 """
805 805 # options
806 806 patch = False
807 807 if opts.get('patch') or opts.get('stat'):
808 808 patch = matchall(repo)
809 809
810 810 tmpl = opts.get('template')
811 811 style = None
812 812 if tmpl:
813 813 tmpl = templater.parsestring(tmpl, quoted=False)
814 814 else:
815 815 style = opts.get('style')
816 816
817 817 # ui settings
818 818 if not (tmpl or style):
819 819 tmpl = ui.config('ui', 'logtemplate')
820 820 if tmpl:
821 821 tmpl = templater.parsestring(tmpl)
822 822 else:
823 823 style = util.expandpath(ui.config('ui', 'style', ''))
824 824
825 825 if not (tmpl or style):
826 826 return changeset_printer(ui, repo, patch, opts, buffered)
827 827
828 828 mapfile = None
829 829 if style and not tmpl:
830 830 mapfile = style
831 831 if not os.path.split(mapfile)[0]:
832 832 mapname = (templater.templatepath('map-cmdline.' + mapfile)
833 833 or templater.templatepath(mapfile))
834 834 if mapname:
835 835 mapfile = mapname
836 836
837 837 try:
838 838 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
839 839 except SyntaxError, inst:
840 840 raise util.Abort(inst.args[0])
841 841 if tmpl:
842 842 t.use_template(tmpl)
843 843 return t
844 844
845 845 def finddate(ui, repo, date):
846 846 """Find the tipmost changeset that matches the given date spec"""
847 847
848 848 df = util.matchdate(date)
849 849 m = matchall(repo)
850 850 results = {}
851 851
852 852 def prep(ctx, fns):
853 853 d = ctx.date()
854 854 if df(d[0]):
855 855 results[ctx.rev()] = d
856 856
857 857 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
858 858 rev = ctx.rev()
859 859 if rev in results:
860 860 ui.status(_("Found revision %s from %s\n") %
861 861 (rev, util.datestr(results[rev])))
862 862 return str(rev)
863 863
864 864 raise util.Abort(_("revision matching date not found"))
865 865
866 866 def walkchangerevs(repo, match, opts, prepare):
867 867 '''Iterate over files and the revs in which they changed.
868 868
869 869 Callers most commonly need to iterate backwards over the history
870 870 in which they are interested. Doing so has awful (quadratic-looking)
871 871 performance, so we use iterators in a "windowed" way.
872 872
873 873 We walk a window of revisions in the desired order. Within the
874 874 window, we first walk forwards to gather data, then in the desired
875 875 order (usually backwards) to display it.
876 876
877 877 This function returns an iterator yielding contexts. Before
878 878 yielding each context, the iterator will first call the prepare
879 879 function on each context in the window in forward order.'''
880 880
881 881 def increasing_windows(start, end, windowsize=8, sizelimit=512):
882 882 if start < end:
883 883 while start < end:
884 884 yield start, min(windowsize, end - start)
885 885 start += windowsize
886 886 if windowsize < sizelimit:
887 887 windowsize *= 2
888 888 else:
889 889 while start > end:
890 890 yield start, min(windowsize, start - end - 1)
891 891 start -= windowsize
892 892 if windowsize < sizelimit:
893 893 windowsize *= 2
894 894
895 895 follow = opts.get('follow') or opts.get('follow_first')
896 896
897 897 if not len(repo):
898 898 return []
899 899
900 900 if follow:
901 901 defrange = '%s:0' % repo['.'].rev()
902 902 else:
903 903 defrange = '-1:0'
904 904 revs = scmutil.revrange(repo, opts['rev'] or [defrange])
905 905 if not revs:
906 906 return []
907 907 wanted = set()
908 908 slowpath = match.anypats() or (match.files() and opts.get('removed'))
909 909 fncache = {}
910 910 change = util.cachefunc(repo.changectx)
911 911
912 912 # First step is to fill wanted, the set of revisions that we want to yield.
913 913 # When it does not induce extra cost, we also fill fncache for revisions in
914 914 # wanted: a cache of filenames that were changed (ctx.files()) and that
915 915 # match the file filtering conditions.
916 916
917 917 if not slowpath and not match.files():
918 918 # No files, no patterns. Display all revs.
919 919 wanted = set(revs)
920 920 copies = []
921 921
922 922 if not slowpath:
923 923 # We only have to read through the filelog to find wanted revisions
924 924
925 925 minrev, maxrev = min(revs), max(revs)
926 926 def filerevgen(filelog, last):
927 927 """
928 928 Only files, no patterns. Check the history of each file.
929 929
930 930 Examines filelog entries within minrev, maxrev linkrev range
931 931 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
932 932 tuples in backwards order
933 933 """
934 934 cl_count = len(repo)
935 935 revs = []
936 936 for j in xrange(0, last + 1):
937 937 linkrev = filelog.linkrev(j)
938 938 if linkrev < minrev:
939 939 continue
940 940 # only yield rev for which we have the changelog, it can
941 941 # happen while doing "hg log" during a pull or commit
942 942 if linkrev >= cl_count:
943 943 break
944 944
945 945 parentlinkrevs = []
946 946 for p in filelog.parentrevs(j):
947 947 if p != nullrev:
948 948 parentlinkrevs.append(filelog.linkrev(p))
949 949 n = filelog.node(j)
950 950 revs.append((linkrev, parentlinkrevs,
951 951 follow and filelog.renamed(n)))
952 952
953 953 return reversed(revs)
954 954 def iterfiles():
955 955 for filename in match.files():
956 956 yield filename, None
957 957 for filename_node in copies:
958 958 yield filename_node
959 959 for file_, node in iterfiles():
960 960 filelog = repo.file(file_)
961 961 if not len(filelog):
962 962 if node is None:
963 963 # A zero count may be a directory or deleted file, so
964 964 # try to find matching entries on the slow path.
965 965 if follow:
966 966 raise util.Abort(
967 967 _('cannot follow nonexistent file: "%s"') % file_)
968 968 slowpath = True
969 969 break
970 970 else:
971 971 continue
972 972
973 973 if node is None:
974 974 last = len(filelog) - 1
975 975 else:
976 976 last = filelog.rev(node)
977 977
978 978
979 979 # keep track of all ancestors of the file
980 980 ancestors = set([filelog.linkrev(last)])
981 981
982 982 # iterate from latest to oldest revision
983 983 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
984 984 if not follow:
985 985 if rev > maxrev:
986 986 continue
987 987 else:
988 988 # Note that last might not be the first interesting
989 989 # rev to us:
990 990 # if the file has been changed after maxrev, we'll
991 991 # have linkrev(last) > maxrev, and we still need
992 992 # to explore the file graph
993 993 if rev not in ancestors:
994 994 continue
995 995 # XXX insert 1327 fix here
996 996 if flparentlinkrevs:
997 997 ancestors.update(flparentlinkrevs)
998 998
999 999 fncache.setdefault(rev, []).append(file_)
1000 1000 wanted.add(rev)
1001 1001 if copied:
1002 1002 copies.append(copied)
1003 1003 if slowpath:
1004 1004 # We have to read the changelog to match filenames against
1005 1005 # changed files
1006 1006
1007 1007 if follow:
1008 1008 raise util.Abort(_('can only follow copies/renames for explicit '
1009 1009 'filenames'))
1010 1010
1011 1011 # The slow path checks files modified in every changeset.
1012 1012 for i in sorted(revs):
1013 1013 ctx = change(i)
1014 1014 matches = filter(match, ctx.files())
1015 1015 if matches:
1016 1016 fncache[i] = matches
1017 1017 wanted.add(i)
1018 1018
1019 1019 class followfilter(object):
1020 1020 def __init__(self, onlyfirst=False):
1021 1021 self.startrev = nullrev
1022 1022 self.roots = set()
1023 1023 self.onlyfirst = onlyfirst
1024 1024
1025 1025 def match(self, rev):
1026 1026 def realparents(rev):
1027 1027 if self.onlyfirst:
1028 1028 return repo.changelog.parentrevs(rev)[0:1]
1029 1029 else:
1030 1030 return filter(lambda x: x != nullrev,
1031 1031 repo.changelog.parentrevs(rev))
1032 1032
1033 1033 if self.startrev == nullrev:
1034 1034 self.startrev = rev
1035 1035 return True
1036 1036
1037 1037 if rev > self.startrev:
1038 1038 # forward: all descendants
1039 1039 if not self.roots:
1040 1040 self.roots.add(self.startrev)
1041 1041 for parent in realparents(rev):
1042 1042 if parent in self.roots:
1043 1043 self.roots.add(rev)
1044 1044 return True
1045 1045 else:
1046 1046 # backwards: all parents
1047 1047 if not self.roots:
1048 1048 self.roots.update(realparents(self.startrev))
1049 1049 if rev in self.roots:
1050 1050 self.roots.remove(rev)
1051 1051 self.roots.update(realparents(rev))
1052 1052 return True
1053 1053
1054 1054 return False
1055 1055
1056 1056 # it might be worthwhile to do this in the iterator if the rev range
1057 1057 # is descending and the prune args are all within that range
1058 1058 for rev in opts.get('prune', ()):
1059 1059 rev = repo.changelog.rev(repo.lookup(rev))
1060 1060 ff = followfilter()
1061 1061 stop = min(revs[0], revs[-1])
1062 1062 for x in xrange(rev, stop - 1, -1):
1063 1063 if ff.match(x):
1064 1064 wanted.discard(x)
1065 1065
1066 1066 # Now that wanted is correctly initialized, we can iterate over the
1067 1067 # revision range, yielding only revisions in wanted.
1068 1068 def iterate():
1069 1069 if follow and not match.files():
1070 1070 ff = followfilter(onlyfirst=opts.get('follow_first'))
1071 1071 def want(rev):
1072 1072 return ff.match(rev) and rev in wanted
1073 1073 else:
1074 1074 def want(rev):
1075 1075 return rev in wanted
1076 1076
1077 1077 for i, window in increasing_windows(0, len(revs)):
1078 1078 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1079 1079 for rev in sorted(nrevs):
1080 1080 fns = fncache.get(rev)
1081 1081 ctx = change(rev)
1082 1082 if not fns:
1083 1083 def fns_generator():
1084 1084 for f in ctx.files():
1085 1085 if match(f):
1086 1086 yield f
1087 1087 fns = fns_generator()
1088 1088 prepare(ctx, fns)
1089 1089 for rev in nrevs:
1090 1090 yield change(rev)
1091 1091 return iterate()
1092 1092
1093 1093 def add(ui, repo, match, dryrun, listsubrepos, prefix):
1094 1094 join = lambda f: os.path.join(prefix, f)
1095 1095 bad = []
1096 1096 oldbad = match.bad
1097 1097 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1098 1098 names = []
1099 1099 wctx = repo[None]
1100 1100 cca = None
1101 1101 abort, warn = scmutil.checkportabilityalert(ui)
1102 1102 if abort or warn:
1103 1103 cca = scmutil.casecollisionauditor(ui, abort, wctx)
1104 1104 for f in repo.walk(match):
1105 1105 exact = match.exact(f)
1106 1106 if exact or f not in repo.dirstate:
1107 1107 if cca:
1108 1108 cca(f)
1109 1109 names.append(f)
1110 1110 if ui.verbose or not exact:
1111 1111 ui.status(_('adding %s\n') % match.rel(join(f)))
1112 1112
1113 1113 if listsubrepos:
1114 1114 for subpath in wctx.substate:
1115 1115 sub = wctx.sub(subpath)
1116 1116 try:
1117 1117 submatch = matchmod.narrowmatcher(subpath, match)
1118 1118 bad.extend(sub.add(ui, submatch, dryrun, prefix))
1119 1119 except error.LookupError:
1120 1120 ui.status(_("skipping missing subrepository: %s\n")
1121 1121 % join(subpath))
1122 1122
1123 1123 if not dryrun:
1124 1124 rejected = wctx.add(names, prefix)
1125 1125 bad.extend(f for f in rejected if f in match.files())
1126 1126 return bad
1127 1127
1128 1128 def commit(ui, repo, commitfunc, pats, opts):
1129 1129 '''commit the specified files or all outstanding changes'''
1130 1130 date = opts.get('date')
1131 1131 if date:
1132 1132 opts['date'] = util.parsedate(date)
1133 1133 message = logmessage(opts)
1134 1134
1135 1135 # extract addremove carefully -- this function can be called from a command
1136 1136 # that doesn't support addremove
1137 1137 if opts.get('addremove'):
1138 1138 addremove(repo, pats, opts)
1139 1139
1140 1140 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1141 1141
1142 1142 def commiteditor(repo, ctx, subs):
1143 1143 if ctx.description():
1144 1144 return ctx.description()
1145 1145 return commitforceeditor(repo, ctx, subs)
1146 1146
1147 1147 def commitforceeditor(repo, ctx, subs):
1148 1148 edittext = []
1149 1149 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1150 1150 if ctx.description():
1151 1151 edittext.append(ctx.description())
1152 1152 edittext.append("")
1153 1153 edittext.append("") # Empty line between message and comments.
1154 1154 edittext.append(_("HG: Enter commit message."
1155 1155 " Lines beginning with 'HG:' are removed."))
1156 1156 edittext.append(_("HG: Leave message empty to abort commit."))
1157 1157 edittext.append("HG: --")
1158 1158 edittext.append(_("HG: user: %s") % ctx.user())
1159 1159 if ctx.p2():
1160 1160 edittext.append(_("HG: branch merge"))
1161 1161 if ctx.branch():
1162 1162 edittext.append(_("HG: branch '%s'") % ctx.branch())
1163 1163 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1164 1164 edittext.extend([_("HG: added %s") % f for f in added])
1165 1165 edittext.extend([_("HG: changed %s") % f for f in modified])
1166 1166 edittext.extend([_("HG: removed %s") % f for f in removed])
1167 1167 if not added and not modified and not removed:
1168 1168 edittext.append(_("HG: no files changed"))
1169 1169 edittext.append("")
1170 1170 # run editor in the repository root
1171 1171 olddir = os.getcwd()
1172 1172 os.chdir(repo.root)
1173 1173 text = repo.ui.edit("\n".join(edittext), ctx.user())
1174 1174 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1175 1175 os.chdir(olddir)
1176 1176
1177 1177 if not text.strip():
1178 1178 raise util.Abort(_("empty commit message"))
1179 1179
1180 1180 return text
1181 1181
1182 1182 def command(table):
1183 1183 '''returns a function object bound to table which can be used as
1184 1184 a decorator for populating table as a command table'''
1185 1185
1186 1186 def cmd(name, options, synopsis=None):
1187 1187 def decorator(func):
1188 1188 if synopsis:
1189 1189 table[name] = func, options, synopsis
1190 1190 else:
1191 1191 table[name] = func, options
1192 1192 return func
1193 1193 return decorator
1194 1194
1195 1195 return cmd
@@ -1,538 +1,690
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright 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 i18n import _
9 import util, error, osutil, revset
10 import os, errno, stat, sys
9 import util, error, osutil, revset, similar
10 import match as matchmod
11 import os, errno, stat, sys, glob
11 12
12 13 def checkfilename(f):
13 14 '''Check that the filename f is an acceptable filename for a tracked file'''
14 15 if '\r' in f or '\n' in f:
15 16 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
16 17
17 18 def checkportable(ui, f):
18 19 '''Check if filename f is portable and warn or abort depending on config'''
19 20 checkfilename(f)
20 21 abort, warn = checkportabilityalert(ui)
21 22 if abort or warn:
22 23 msg = util.checkwinfilename(f)
23 24 if msg:
24 25 msg = "%s: %r" % (msg, f)
25 26 if abort:
26 27 raise util.Abort(msg)
27 28 ui.warn(_("warning: %s\n") % msg)
28 29
29 30 def checkportabilityalert(ui):
30 31 '''check if the user's config requests nothing, a warning, or abort for
31 32 non-portable filenames'''
32 33 val = ui.config('ui', 'portablefilenames', 'warn')
33 34 lval = val.lower()
34 35 bval = util.parsebool(val)
35 36 abort = os.name == 'nt' or lval == 'abort'
36 37 warn = bval or lval == 'warn'
37 38 if bval is None and not (warn or abort or lval == 'ignore'):
38 39 raise error.ConfigError(
39 40 _("ui.portablefilenames value is invalid ('%s')") % val)
40 41 return abort, warn
41 42
42 43 class casecollisionauditor(object):
43 44 def __init__(self, ui, abort, existingiter):
44 45 self._ui = ui
45 46 self._abort = abort
46 47 self._map = {}
47 48 for f in existingiter:
48 49 self._map[f.lower()] = f
49 50
50 51 def __call__(self, f):
51 52 fl = f.lower()
52 53 map = self._map
53 54 if fl in map and map[fl] != f:
54 55 msg = _('possible case-folding collision for %s') % f
55 56 if self._abort:
56 57 raise util.Abort(msg)
57 58 self._ui.warn(_("warning: %s\n") % msg)
58 59 map[fl] = f
59 60
60 61 class pathauditor(object):
61 62 '''ensure that a filesystem path contains no banned components.
62 63 the following properties of a path are checked:
63 64
64 65 - ends with a directory separator
65 66 - under top-level .hg
66 67 - starts at the root of a windows drive
67 68 - contains ".."
68 69 - traverses a symlink (e.g. a/symlink_here/b)
69 70 - inside a nested repository (a callback can be used to approve
70 71 some nested repositories, e.g., subrepositories)
71 72 '''
72 73
73 74 def __init__(self, root, callback=None):
74 75 self.audited = set()
75 76 self.auditeddir = set()
76 77 self.root = root
77 78 self.callback = callback
78 79
79 80 def __call__(self, path):
80 81 '''Check the relative path.
81 82 path may contain a pattern (e.g. foodir/**.txt)'''
82 83
83 84 if path in self.audited:
84 85 return
85 86 # AIX ignores "/" at end of path, others raise EISDIR.
86 87 if util.endswithsep(path):
87 88 raise util.Abort(_("path ends in directory separator: %s") % path)
88 89 normpath = os.path.normcase(path)
89 90 parts = util.splitpath(normpath)
90 91 if (os.path.splitdrive(path)[0]
91 92 or parts[0].lower() in ('.hg', '.hg.', '')
92 93 or os.pardir in parts):
93 94 raise util.Abort(_("path contains illegal component: %s") % path)
94 95 if '.hg' in path.lower():
95 96 lparts = [p.lower() for p in parts]
96 97 for p in '.hg', '.hg.':
97 98 if p in lparts[1:]:
98 99 pos = lparts.index(p)
99 100 base = os.path.join(*parts[:pos])
100 101 raise util.Abort(_('path %r is inside nested repo %r')
101 102 % (path, base))
102 103
103 104 parts.pop()
104 105 prefixes = []
105 106 while parts:
106 107 prefix = os.sep.join(parts)
107 108 if prefix in self.auditeddir:
108 109 break
109 110 curpath = os.path.join(self.root, prefix)
110 111 try:
111 112 st = os.lstat(curpath)
112 113 except OSError, err:
113 114 # EINVAL can be raised as invalid path syntax under win32.
114 115 # They must be ignored for patterns can be checked too.
115 116 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
116 117 raise
117 118 else:
118 119 if stat.S_ISLNK(st.st_mode):
119 120 raise util.Abort(
120 121 _('path %r traverses symbolic link %r')
121 122 % (path, prefix))
122 123 elif (stat.S_ISDIR(st.st_mode) and
123 124 os.path.isdir(os.path.join(curpath, '.hg'))):
124 125 if not self.callback or not self.callback(curpath):
125 126 raise util.Abort(_('path %r is inside nested repo %r') %
126 127 (path, prefix))
127 128 prefixes.append(prefix)
128 129 parts.pop()
129 130
130 131 self.audited.add(path)
131 132 # only add prefixes to the cache after checking everything: we don't
132 133 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
133 134 self.auditeddir.update(prefixes)
134 135
135 136 class abstractopener(object):
136 137 """Abstract base class; cannot be instantiated"""
137 138
138 139 def __init__(self, *args, **kwargs):
139 140 '''Prevent instantiation; don't call this from subclasses.'''
140 141 raise NotImplementedError('attempted instantiating ' + str(type(self)))
141 142
142 143 def read(self, path):
143 144 fp = self(path, 'rb')
144 145 try:
145 146 return fp.read()
146 147 finally:
147 148 fp.close()
148 149
149 150 def write(self, path, data):
150 151 fp = self(path, 'wb')
151 152 try:
152 153 return fp.write(data)
153 154 finally:
154 155 fp.close()
155 156
156 157 def append(self, path, data):
157 158 fp = self(path, 'ab')
158 159 try:
159 160 return fp.write(data)
160 161 finally:
161 162 fp.close()
162 163
163 164 class opener(abstractopener):
164 165 '''Open files relative to a base directory
165 166
166 167 This class is used to hide the details of COW semantics and
167 168 remote file access from higher level code.
168 169 '''
169 170 def __init__(self, base, audit=True):
170 171 self.base = base
171 172 if audit:
172 173 self.auditor = pathauditor(base)
173 174 else:
174 175 self.auditor = util.always
175 176 self.createmode = None
176 177 self._trustnlink = None
177 178
178 179 @util.propertycache
179 180 def _cansymlink(self):
180 181 return util.checklink(self.base)
181 182
182 183 def _fixfilemode(self, name):
183 184 if self.createmode is None:
184 185 return
185 186 os.chmod(name, self.createmode & 0666)
186 187
187 188 def __call__(self, path, mode="r", text=False, atomictemp=False):
188 189 r = util.checkosfilename(path)
189 190 if r:
190 191 raise util.Abort("%s: %r" % (r, path))
191 192 self.auditor(path)
192 193 f = os.path.join(self.base, path)
193 194
194 195 if not text and "b" not in mode:
195 196 mode += "b" # for that other OS
196 197
197 198 nlink = -1
198 199 dirname, basename = os.path.split(f)
199 200 # If basename is empty, then the path is malformed because it points
200 201 # to a directory. Let the posixfile() call below raise IOError.
201 202 if basename and mode not in ('r', 'rb'):
202 203 if atomictemp:
203 204 if not os.path.isdir(dirname):
204 205 util.makedirs(dirname, self.createmode)
205 206 return util.atomictempfile(f, mode, self.createmode)
206 207 try:
207 208 if 'w' in mode:
208 209 util.unlink(f)
209 210 nlink = 0
210 211 else:
211 212 # nlinks() may behave differently for files on Windows
212 213 # shares if the file is open.
213 214 fd = util.posixfile(f)
214 215 nlink = util.nlinks(f)
215 216 if nlink < 1:
216 217 nlink = 2 # force mktempcopy (issue1922)
217 218 fd.close()
218 219 except (OSError, IOError), e:
219 220 if e.errno != errno.ENOENT:
220 221 raise
221 222 nlink = 0
222 223 if not os.path.isdir(dirname):
223 224 util.makedirs(dirname, self.createmode)
224 225 if nlink > 0:
225 226 if self._trustnlink is None:
226 227 self._trustnlink = nlink > 1 or util.checknlink(f)
227 228 if nlink > 1 or not self._trustnlink:
228 229 util.rename(util.mktempcopy(f), f)
229 230 fp = util.posixfile(f, mode)
230 231 if nlink == 0:
231 232 self._fixfilemode(f)
232 233 return fp
233 234
234 235 def symlink(self, src, dst):
235 236 self.auditor(dst)
236 237 linkname = os.path.join(self.base, dst)
237 238 try:
238 239 os.unlink(linkname)
239 240 except OSError:
240 241 pass
241 242
242 243 dirname = os.path.dirname(linkname)
243 244 if not os.path.exists(dirname):
244 245 util.makedirs(dirname, self.createmode)
245 246
246 247 if self._cansymlink:
247 248 try:
248 249 os.symlink(src, linkname)
249 250 except OSError, err:
250 251 raise OSError(err.errno, _('could not symlink to %r: %s') %
251 252 (src, err.strerror), linkname)
252 253 else:
253 254 f = self(dst, "w")
254 255 f.write(src)
255 256 f.close()
256 257 self._fixfilemode(dst)
257 258
258 259 class filteropener(abstractopener):
259 260 '''Wrapper opener for filtering filenames with a function.'''
260 261
261 262 def __init__(self, opener, filter):
262 263 self._filter = filter
263 264 self._orig = opener
264 265
265 266 def __call__(self, path, *args, **kwargs):
266 267 return self._orig(self._filter(path), *args, **kwargs)
267 268
268 269 def canonpath(root, cwd, myname, auditor=None):
269 270 '''return the canonical path of myname, given cwd and root'''
270 271 if util.endswithsep(root):
271 272 rootsep = root
272 273 else:
273 274 rootsep = root + os.sep
274 275 name = myname
275 276 if not os.path.isabs(name):
276 277 name = os.path.join(root, cwd, name)
277 278 name = os.path.normpath(name)
278 279 if auditor is None:
279 280 auditor = pathauditor(root)
280 281 if name != rootsep and name.startswith(rootsep):
281 282 name = name[len(rootsep):]
282 283 auditor(name)
283 284 return util.pconvert(name)
284 285 elif name == root:
285 286 return ''
286 287 else:
287 288 # Determine whether `name' is in the hierarchy at or beneath `root',
288 289 # by iterating name=dirname(name) until that causes no change (can't
289 290 # check name == '/', because that doesn't work on windows). For each
290 291 # `name', compare dev/inode numbers. If they match, the list `rel'
291 292 # holds the reversed list of components making up the relative file
292 293 # name we want.
293 294 root_st = os.stat(root)
294 295 rel = []
295 296 while True:
296 297 try:
297 298 name_st = os.stat(name)
298 299 except OSError:
299 300 break
300 301 if util.samestat(name_st, root_st):
301 302 if not rel:
302 303 # name was actually the same as root (maybe a symlink)
303 304 return ''
304 305 rel.reverse()
305 306 name = os.path.join(*rel)
306 307 auditor(name)
307 308 return util.pconvert(name)
308 309 dirname, basename = os.path.split(name)
309 310 rel.append(basename)
310 311 if dirname == name:
311 312 break
312 313 name = dirname
313 314
314 315 raise util.Abort('%s not under root' % myname)
315 316
316 317 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
317 318 '''yield every hg repository under path, recursively.'''
318 319 def errhandler(err):
319 320 if err.filename == path:
320 321 raise err
321 322 if followsym and hasattr(os.path, 'samestat'):
322 323 def adddir(dirlst, dirname):
323 324 match = False
324 325 samestat = os.path.samestat
325 326 dirstat = os.stat(dirname)
326 327 for lstdirstat in dirlst:
327 328 if samestat(dirstat, lstdirstat):
328 329 match = True
329 330 break
330 331 if not match:
331 332 dirlst.append(dirstat)
332 333 return not match
333 334 else:
334 335 followsym = False
335 336
336 337 if (seen_dirs is None) and followsym:
337 338 seen_dirs = []
338 339 adddir(seen_dirs, path)
339 340 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
340 341 dirs.sort()
341 342 if '.hg' in dirs:
342 343 yield root # found a repository
343 344 qroot = os.path.join(root, '.hg', 'patches')
344 345 if os.path.isdir(os.path.join(qroot, '.hg')):
345 346 yield qroot # we have a patch queue repo here
346 347 if recurse:
347 348 # avoid recursing inside the .hg directory
348 349 dirs.remove('.hg')
349 350 else:
350 351 dirs[:] = [] # don't descend further
351 352 elif followsym:
352 353 newdirs = []
353 354 for d in dirs:
354 355 fname = os.path.join(root, d)
355 356 if adddir(seen_dirs, fname):
356 357 if os.path.islink(fname):
357 358 for hgname in walkrepos(fname, True, seen_dirs):
358 359 yield hgname
359 360 else:
360 361 newdirs.append(d)
361 362 dirs[:] = newdirs
362 363
363 364 def osrcpath():
364 365 '''return default os-specific hgrc search path'''
365 366 path = systemrcpath()
366 367 path.extend(userrcpath())
367 368 path = [os.path.normpath(f) for f in path]
368 369 return path
369 370
370 371 _rcpath = None
371 372
372 373 def rcpath():
373 374 '''return hgrc search path. if env var HGRCPATH is set, use it.
374 375 for each item in path, if directory, use files ending in .rc,
375 376 else use item.
376 377 make HGRCPATH empty to only look in .hg/hgrc of current repo.
377 378 if no HGRCPATH, use default os-specific path.'''
378 379 global _rcpath
379 380 if _rcpath is None:
380 381 if 'HGRCPATH' in os.environ:
381 382 _rcpath = []
382 383 for p in os.environ['HGRCPATH'].split(os.pathsep):
383 384 if not p:
384 385 continue
385 386 p = util.expandpath(p)
386 387 if os.path.isdir(p):
387 388 for f, kind in osutil.listdir(p):
388 389 if f.endswith('.rc'):
389 390 _rcpath.append(os.path.join(p, f))
390 391 else:
391 392 _rcpath.append(p)
392 393 else:
393 394 _rcpath = osrcpath()
394 395 return _rcpath
395 396
396 397 if os.name != 'nt':
397 398
398 399 def rcfiles(path):
399 400 rcs = [os.path.join(path, 'hgrc')]
400 401 rcdir = os.path.join(path, 'hgrc.d')
401 402 try:
402 403 rcs.extend([os.path.join(rcdir, f)
403 404 for f, kind in osutil.listdir(rcdir)
404 405 if f.endswith(".rc")])
405 406 except OSError:
406 407 pass
407 408 return rcs
408 409
409 410 def systemrcpath():
410 411 path = []
411 412 # old mod_python does not set sys.argv
412 413 if len(getattr(sys, 'argv', [])) > 0:
413 414 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
414 415 '/../etc/mercurial'))
415 416 path.extend(rcfiles('/etc/mercurial'))
416 417 return path
417 418
418 419 def userrcpath():
419 420 return [os.path.expanduser('~/.hgrc')]
420 421
421 422 else:
422 423
423 424 _HKEY_LOCAL_MACHINE = 0x80000002L
424 425
425 426 def systemrcpath():
426 427 '''return default os-specific hgrc search path'''
427 428 rcpath = []
428 429 filename = util.executablepath()
429 430 # Use mercurial.ini found in directory with hg.exe
430 431 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
431 432 if os.path.isfile(progrc):
432 433 rcpath.append(progrc)
433 434 return rcpath
434 435 # Use hgrc.d found in directory with hg.exe
435 436 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
436 437 if os.path.isdir(progrcd):
437 438 for f, kind in osutil.listdir(progrcd):
438 439 if f.endswith('.rc'):
439 440 rcpath.append(os.path.join(progrcd, f))
440 441 return rcpath
441 442 # else look for a system rcpath in the registry
442 443 value = util.lookupreg('SOFTWARE\\Mercurial', None,
443 444 _HKEY_LOCAL_MACHINE)
444 445 if not isinstance(value, str) or not value:
445 446 return rcpath
446 447 value = value.replace('/', os.sep)
447 448 for p in value.split(os.pathsep):
448 449 if p.lower().endswith('mercurial.ini'):
449 450 rcpath.append(p)
450 451 elif os.path.isdir(p):
451 452 for f, kind in osutil.listdir(p):
452 453 if f.endswith('.rc'):
453 454 rcpath.append(os.path.join(p, f))
454 455 return rcpath
455 456
456 457 def userrcpath():
457 458 '''return os-specific hgrc search path to the user dir'''
458 459 home = os.path.expanduser('~')
459 460 path = [os.path.join(home, 'mercurial.ini'),
460 461 os.path.join(home, '.hgrc')]
461 462 userprofile = os.environ.get('USERPROFILE')
462 463 if userprofile:
463 464 path.append(os.path.join(userprofile, 'mercurial.ini'))
464 465 path.append(os.path.join(userprofile, '.hgrc'))
465 466 return path
466 467
467 468 def revsingle(repo, revspec, default='.'):
468 469 if not revspec:
469 470 return repo[default]
470 471
471 472 l = revrange(repo, [revspec])
472 473 if len(l) < 1:
473 474 raise util.Abort(_('empty revision set'))
474 475 return repo[l[-1]]
475 476
476 477 def revpair(repo, revs):
477 478 if not revs:
478 479 return repo.dirstate.p1(), None
479 480
480 481 l = revrange(repo, revs)
481 482
482 483 if len(l) == 0:
483 484 return repo.dirstate.p1(), None
484 485
485 486 if len(l) == 1:
486 487 return repo.lookup(l[0]), None
487 488
488 489 return repo.lookup(l[0]), repo.lookup(l[-1])
489 490
490 491 _revrangesep = ':'
491 492
492 493 def revrange(repo, revs):
493 494 """Yield revision as strings from a list of revision specifications."""
494 495
495 496 def revfix(repo, val, defval):
496 497 if not val and val != 0 and defval is not None:
497 498 return defval
498 499 return repo.changelog.rev(repo.lookup(val))
499 500
500 501 seen, l = set(), []
501 502 for spec in revs:
502 503 # attempt to parse old-style ranges first to deal with
503 504 # things like old-tag which contain query metacharacters
504 505 try:
505 506 if isinstance(spec, int):
506 507 seen.add(spec)
507 508 l.append(spec)
508 509 continue
509 510
510 511 if _revrangesep in spec:
511 512 start, end = spec.split(_revrangesep, 1)
512 513 start = revfix(repo, start, 0)
513 514 end = revfix(repo, end, len(repo) - 1)
514 515 step = start > end and -1 or 1
515 516 for rev in xrange(start, end + step, step):
516 517 if rev in seen:
517 518 continue
518 519 seen.add(rev)
519 520 l.append(rev)
520 521 continue
521 522 elif spec and spec in repo: # single unquoted rev
522 523 rev = revfix(repo, spec, None)
523 524 if rev in seen:
524 525 continue
525 526 seen.add(rev)
526 527 l.append(rev)
527 528 continue
528 529 except error.RepoLookupError:
529 530 pass
530 531
531 532 # fall through to new-style queries if old-style fails
532 533 m = revset.match(repo.ui, spec)
533 534 for r in m(repo, range(len(repo))):
534 535 if r not in seen:
535 536 l.append(r)
536 537 seen.update(l)
537 538
538 539 return l
540
541 def expandpats(pats):
542 if not util.expandglobs:
543 return list(pats)
544 ret = []
545 for p in pats:
546 kind, name = matchmod._patsplit(p, None)
547 if kind is None:
548 try:
549 globbed = glob.glob(name)
550 except re.error:
551 globbed = [name]
552 if globbed:
553 ret.extend(globbed)
554 continue
555 ret.append(p)
556 return ret
557
558 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
559 if pats == ("",):
560 pats = []
561 if not globbed and default == 'relpath':
562 pats = expandpats(pats or [])
563 m = matchmod.match(repo.root, repo.getcwd(), pats,
564 opts.get('include'), opts.get('exclude'), default,
565 auditor=repo.auditor)
566 def badfn(f, msg):
567 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
568 m.bad = badfn
569 return m
570
571 def matchall(repo):
572 return matchmod.always(repo.root, repo.getcwd())
573
574 def matchfiles(repo, files):
575 return matchmod.exact(repo.root, repo.getcwd(), files)
576
577 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
578 if dry_run is None:
579 dry_run = opts.get('dry_run')
580 if similarity is None:
581 similarity = float(opts.get('similarity') or 0)
582 # we'd use status here, except handling of symlinks and ignore is tricky
583 added, unknown, deleted, removed = [], [], [], []
584 audit_path = pathauditor(repo.root)
585 m = match(repo, pats, opts)
586 for abs in repo.walk(m):
587 target = repo.wjoin(abs)
588 good = True
589 try:
590 audit_path(abs)
591 except (OSError, util.Abort):
592 good = False
593 rel = m.rel(abs)
594 exact = m.exact(abs)
595 if good and abs not in repo.dirstate:
596 unknown.append(abs)
597 if repo.ui.verbose or not exact:
598 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
599 elif repo.dirstate[abs] != 'r' and (not good or not os.path.lexists(target)
600 or (os.path.isdir(target) and not os.path.islink(target))):
601 deleted.append(abs)
602 if repo.ui.verbose or not exact:
603 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
604 # for finding renames
605 elif repo.dirstate[abs] == 'r':
606 removed.append(abs)
607 elif repo.dirstate[abs] == 'a':
608 added.append(abs)
609 copies = {}
610 if similarity > 0:
611 for old, new, score in similar.findrenames(repo,
612 added + unknown, removed + deleted, similarity):
613 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
614 repo.ui.status(_('recording removal of %s as rename to %s '
615 '(%d%% similar)\n') %
616 (m.rel(old), m.rel(new), score * 100))
617 copies[new] = old
618
619 if not dry_run:
620 wctx = repo[None]
621 wlock = repo.wlock()
622 try:
623 wctx.remove(deleted)
624 wctx.add(unknown)
625 for new, old in copies.iteritems():
626 wctx.copy(old, new)
627 finally:
628 wlock.release()
629
630 def updatedir(ui, repo, patches, similarity=0):
631 '''Update dirstate after patch application according to metadata'''
632 if not patches:
633 return []
634 copies = []
635 removes = set()
636 cfiles = patches.keys()
637 cwd = repo.getcwd()
638 if cwd:
639 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
640 for f in patches:
641 gp = patches[f]
642 if not gp:
643 continue
644 if gp.op == 'RENAME':
645 copies.append((gp.oldpath, gp.path))
646 removes.add(gp.oldpath)
647 elif gp.op == 'COPY':
648 copies.append((gp.oldpath, gp.path))
649 elif gp.op == 'DELETE':
650 removes.add(gp.path)
651
652 wctx = repo[None]
653 for src, dst in copies:
654 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
655 if (not similarity) and removes:
656 wctx.remove(sorted(removes), True)
657
658 for f in patches:
659 gp = patches[f]
660 if gp and gp.mode:
661 islink, isexec = gp.mode
662 dst = repo.wjoin(gp.path)
663 # patch won't create empty files
664 if gp.op == 'ADD' and not os.path.lexists(dst):
665 flags = (isexec and 'x' or '') + (islink and 'l' or '')
666 repo.wwrite(gp.path, '', flags)
667 util.setflags(dst, islink, isexec)
668 addremove(repo, cfiles, similarity=similarity)
669 files = patches.keys()
670 files.extend([r for r in removes if r not in files])
671 return sorted(files)
672
673 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
674 """Update the dirstate to reflect the intent of copying src to dst. For
675 different reasons it might not end with dst being marked as copied from src.
676 """
677 origsrc = repo.dirstate.copied(src) or src
678 if dst == origsrc: # copying back a copy?
679 if repo.dirstate[dst] not in 'mn' and not dryrun:
680 repo.dirstate.normallookup(dst)
681 else:
682 if repo.dirstate[origsrc] == 'a' and origsrc == src:
683 if not ui.quiet:
684 ui.warn(_("%s has not been committed yet, so no copy "
685 "data will be stored for %s.\n")
686 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
687 if repo.dirstate[dst] in '?r' and not dryrun:
688 wctx.add([dst])
689 elif not dryrun:
690 wctx.copy(origsrc, dst)
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now