##// END OF EJS Templates
scmutil: update cached copy when filecached attribute is assigned (issue3263)...
Idan Kamara -
r16115:236bb604 stable
parent child Browse files
Show More
@@ -1,822 +1,837
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 9 import util, error, osutil, revset, similar, encoding
10 10 import match as matchmod
11 11 import os, errno, re, stat, sys, glob
12 12
13 13 def nochangesfound(ui, secretlist=None):
14 14 '''report no changes for push/pull'''
15 15 if secretlist:
16 16 ui.status(_("no changes found (ignored %d secret changesets)\n")
17 17 % len(secretlist))
18 18 else:
19 19 ui.status(_("no changes found\n"))
20 20
21 21 def checkfilename(f):
22 22 '''Check that the filename f is an acceptable filename for a tracked file'''
23 23 if '\r' in f or '\n' in f:
24 24 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
25 25
26 26 def checkportable(ui, f):
27 27 '''Check if filename f is portable and warn or abort depending on config'''
28 28 checkfilename(f)
29 29 abort, warn = checkportabilityalert(ui)
30 30 if abort or warn:
31 31 msg = util.checkwinfilename(f)
32 32 if msg:
33 33 msg = "%s: %r" % (msg, f)
34 34 if abort:
35 35 raise util.Abort(msg)
36 36 ui.warn(_("warning: %s\n") % msg)
37 37
38 38 def checkportabilityalert(ui):
39 39 '''check if the user's config requests nothing, a warning, or abort for
40 40 non-portable filenames'''
41 41 val = ui.config('ui', 'portablefilenames', 'warn')
42 42 lval = val.lower()
43 43 bval = util.parsebool(val)
44 44 abort = os.name == 'nt' or lval == 'abort'
45 45 warn = bval or lval == 'warn'
46 46 if bval is None and not (warn or abort or lval == 'ignore'):
47 47 raise error.ConfigError(
48 48 _("ui.portablefilenames value is invalid ('%s')") % val)
49 49 return abort, warn
50 50
51 51 class casecollisionauditor(object):
52 52 def __init__(self, ui, abort, existingiter):
53 53 self._ui = ui
54 54 self._abort = abort
55 55 self._map = {}
56 56 for f in existingiter:
57 57 self._map[encoding.lower(f)] = f
58 58
59 59 def __call__(self, f):
60 60 fl = encoding.lower(f)
61 61 map = self._map
62 62 if fl in map and map[fl] != f:
63 63 msg = _('possible case-folding collision for %s') % f
64 64 if self._abort:
65 65 raise util.Abort(msg)
66 66 self._ui.warn(_("warning: %s\n") % msg)
67 67 map[fl] = f
68 68
69 69 class pathauditor(object):
70 70 '''ensure that a filesystem path contains no banned components.
71 71 the following properties of a path are checked:
72 72
73 73 - ends with a directory separator
74 74 - under top-level .hg
75 75 - starts at the root of a windows drive
76 76 - contains ".."
77 77 - traverses a symlink (e.g. a/symlink_here/b)
78 78 - inside a nested repository (a callback can be used to approve
79 79 some nested repositories, e.g., subrepositories)
80 80 '''
81 81
82 82 def __init__(self, root, callback=None):
83 83 self.audited = set()
84 84 self.auditeddir = set()
85 85 self.root = root
86 86 self.callback = callback
87 87 if os.path.lexists(root) and not util.checkcase(root):
88 88 self.normcase = util.normcase
89 89 else:
90 90 self.normcase = lambda x: x
91 91
92 92 def __call__(self, path):
93 93 '''Check the relative path.
94 94 path may contain a pattern (e.g. foodir/**.txt)'''
95 95
96 96 path = util.localpath(path)
97 97 normpath = self.normcase(path)
98 98 if normpath in self.audited:
99 99 return
100 100 # AIX ignores "/" at end of path, others raise EISDIR.
101 101 if util.endswithsep(path):
102 102 raise util.Abort(_("path ends in directory separator: %s") % path)
103 103 parts = util.splitpath(path)
104 104 if (os.path.splitdrive(path)[0]
105 105 or parts[0].lower() in ('.hg', '.hg.', '')
106 106 or os.pardir in parts):
107 107 raise util.Abort(_("path contains illegal component: %s") % path)
108 108 if '.hg' in path.lower():
109 109 lparts = [p.lower() for p in parts]
110 110 for p in '.hg', '.hg.':
111 111 if p in lparts[1:]:
112 112 pos = lparts.index(p)
113 113 base = os.path.join(*parts[:pos])
114 114 raise util.Abort(_("path '%s' is inside nested repo %r")
115 115 % (path, base))
116 116
117 117 normparts = util.splitpath(normpath)
118 118 assert len(parts) == len(normparts)
119 119
120 120 parts.pop()
121 121 normparts.pop()
122 122 prefixes = []
123 123 while parts:
124 124 prefix = os.sep.join(parts)
125 125 normprefix = os.sep.join(normparts)
126 126 if normprefix in self.auditeddir:
127 127 break
128 128 curpath = os.path.join(self.root, prefix)
129 129 try:
130 130 st = os.lstat(curpath)
131 131 except OSError, err:
132 132 # EINVAL can be raised as invalid path syntax under win32.
133 133 # They must be ignored for patterns can be checked too.
134 134 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
135 135 raise
136 136 else:
137 137 if stat.S_ISLNK(st.st_mode):
138 138 raise util.Abort(
139 139 _('path %r traverses symbolic link %r')
140 140 % (path, prefix))
141 141 elif (stat.S_ISDIR(st.st_mode) and
142 142 os.path.isdir(os.path.join(curpath, '.hg'))):
143 143 if not self.callback or not self.callback(curpath):
144 144 raise util.Abort(_("path '%s' is inside nested repo %r") %
145 145 (path, prefix))
146 146 prefixes.append(normprefix)
147 147 parts.pop()
148 148 normparts.pop()
149 149
150 150 self.audited.add(normpath)
151 151 # only add prefixes to the cache after checking everything: we don't
152 152 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
153 153 self.auditeddir.update(prefixes)
154 154
155 155 class abstractopener(object):
156 156 """Abstract base class; cannot be instantiated"""
157 157
158 158 def __init__(self, *args, **kwargs):
159 159 '''Prevent instantiation; don't call this from subclasses.'''
160 160 raise NotImplementedError('attempted instantiating ' + str(type(self)))
161 161
162 162 def read(self, path):
163 163 fp = self(path, 'rb')
164 164 try:
165 165 return fp.read()
166 166 finally:
167 167 fp.close()
168 168
169 169 def write(self, path, data):
170 170 fp = self(path, 'wb')
171 171 try:
172 172 return fp.write(data)
173 173 finally:
174 174 fp.close()
175 175
176 176 def append(self, path, data):
177 177 fp = self(path, 'ab')
178 178 try:
179 179 return fp.write(data)
180 180 finally:
181 181 fp.close()
182 182
183 183 class opener(abstractopener):
184 184 '''Open files relative to a base directory
185 185
186 186 This class is used to hide the details of COW semantics and
187 187 remote file access from higher level code.
188 188 '''
189 189 def __init__(self, base, audit=True):
190 190 self.base = base
191 191 self._audit = audit
192 192 if audit:
193 193 self.auditor = pathauditor(base)
194 194 else:
195 195 self.auditor = util.always
196 196 self.createmode = None
197 197 self._trustnlink = None
198 198
199 199 @util.propertycache
200 200 def _cansymlink(self):
201 201 return util.checklink(self.base)
202 202
203 203 def _fixfilemode(self, name):
204 204 if self.createmode is None:
205 205 return
206 206 os.chmod(name, self.createmode & 0666)
207 207
208 208 def __call__(self, path, mode="r", text=False, atomictemp=False):
209 209 if self._audit:
210 210 r = util.checkosfilename(path)
211 211 if r:
212 212 raise util.Abort("%s: %r" % (r, path))
213 213 self.auditor(path)
214 214 f = os.path.join(self.base, path)
215 215
216 216 if not text and "b" not in mode:
217 217 mode += "b" # for that other OS
218 218
219 219 nlink = -1
220 220 dirname, basename = os.path.split(f)
221 221 # If basename is empty, then the path is malformed because it points
222 222 # to a directory. Let the posixfile() call below raise IOError.
223 223 if basename and mode not in ('r', 'rb'):
224 224 if atomictemp:
225 225 if not os.path.isdir(dirname):
226 226 util.makedirs(dirname, self.createmode)
227 227 return util.atomictempfile(f, mode, self.createmode)
228 228 try:
229 229 if 'w' in mode:
230 230 util.unlink(f)
231 231 nlink = 0
232 232 else:
233 233 # nlinks() may behave differently for files on Windows
234 234 # shares if the file is open.
235 235 fd = util.posixfile(f)
236 236 nlink = util.nlinks(f)
237 237 if nlink < 1:
238 238 nlink = 2 # force mktempcopy (issue1922)
239 239 fd.close()
240 240 except (OSError, IOError), e:
241 241 if e.errno != errno.ENOENT:
242 242 raise
243 243 nlink = 0
244 244 if not os.path.isdir(dirname):
245 245 util.makedirs(dirname, self.createmode)
246 246 if nlink > 0:
247 247 if self._trustnlink is None:
248 248 self._trustnlink = nlink > 1 or util.checknlink(f)
249 249 if nlink > 1 or not self._trustnlink:
250 250 util.rename(util.mktempcopy(f), f)
251 251 fp = util.posixfile(f, mode)
252 252 if nlink == 0:
253 253 self._fixfilemode(f)
254 254 return fp
255 255
256 256 def symlink(self, src, dst):
257 257 self.auditor(dst)
258 258 linkname = os.path.join(self.base, dst)
259 259 try:
260 260 os.unlink(linkname)
261 261 except OSError:
262 262 pass
263 263
264 264 dirname = os.path.dirname(linkname)
265 265 if not os.path.exists(dirname):
266 266 util.makedirs(dirname, self.createmode)
267 267
268 268 if self._cansymlink:
269 269 try:
270 270 os.symlink(src, linkname)
271 271 except OSError, err:
272 272 raise OSError(err.errno, _('could not symlink to %r: %s') %
273 273 (src, err.strerror), linkname)
274 274 else:
275 275 f = self(dst, "w")
276 276 f.write(src)
277 277 f.close()
278 278 self._fixfilemode(dst)
279 279
280 280 def audit(self, path):
281 281 self.auditor(path)
282 282
283 283 class filteropener(abstractopener):
284 284 '''Wrapper opener for filtering filenames with a function.'''
285 285
286 286 def __init__(self, opener, filter):
287 287 self._filter = filter
288 288 self._orig = opener
289 289
290 290 def __call__(self, path, *args, **kwargs):
291 291 return self._orig(self._filter(path), *args, **kwargs)
292 292
293 293 def canonpath(root, cwd, myname, auditor=None):
294 294 '''return the canonical path of myname, given cwd and root'''
295 295 if util.endswithsep(root):
296 296 rootsep = root
297 297 else:
298 298 rootsep = root + os.sep
299 299 name = myname
300 300 if not os.path.isabs(name):
301 301 name = os.path.join(root, cwd, name)
302 302 name = os.path.normpath(name)
303 303 if auditor is None:
304 304 auditor = pathauditor(root)
305 305 if name != rootsep and name.startswith(rootsep):
306 306 name = name[len(rootsep):]
307 307 auditor(name)
308 308 return util.pconvert(name)
309 309 elif name == root:
310 310 return ''
311 311 else:
312 312 # Determine whether `name' is in the hierarchy at or beneath `root',
313 313 # by iterating name=dirname(name) until that causes no change (can't
314 314 # check name == '/', because that doesn't work on windows). For each
315 315 # `name', compare dev/inode numbers. If they match, the list `rel'
316 316 # holds the reversed list of components making up the relative file
317 317 # name we want.
318 318 root_st = os.stat(root)
319 319 rel = []
320 320 while True:
321 321 try:
322 322 name_st = os.stat(name)
323 323 except OSError:
324 324 name_st = None
325 325 if name_st and util.samestat(name_st, root_st):
326 326 if not rel:
327 327 # name was actually the same as root (maybe a symlink)
328 328 return ''
329 329 rel.reverse()
330 330 name = os.path.join(*rel)
331 331 auditor(name)
332 332 return util.pconvert(name)
333 333 dirname, basename = os.path.split(name)
334 334 rel.append(basename)
335 335 if dirname == name:
336 336 break
337 337 name = dirname
338 338
339 339 raise util.Abort('%s not under root' % myname)
340 340
341 341 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
342 342 '''yield every hg repository under path, recursively.'''
343 343 def errhandler(err):
344 344 if err.filename == path:
345 345 raise err
346 346 samestat = getattr(os.path, 'samestat', None)
347 347 if followsym and samestat is not None:
348 348 def adddir(dirlst, dirname):
349 349 match = False
350 350 dirstat = os.stat(dirname)
351 351 for lstdirstat in dirlst:
352 352 if samestat(dirstat, lstdirstat):
353 353 match = True
354 354 break
355 355 if not match:
356 356 dirlst.append(dirstat)
357 357 return not match
358 358 else:
359 359 followsym = False
360 360
361 361 if (seen_dirs is None) and followsym:
362 362 seen_dirs = []
363 363 adddir(seen_dirs, path)
364 364 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
365 365 dirs.sort()
366 366 if '.hg' in dirs:
367 367 yield root # found a repository
368 368 qroot = os.path.join(root, '.hg', 'patches')
369 369 if os.path.isdir(os.path.join(qroot, '.hg')):
370 370 yield qroot # we have a patch queue repo here
371 371 if recurse:
372 372 # avoid recursing inside the .hg directory
373 373 dirs.remove('.hg')
374 374 else:
375 375 dirs[:] = [] # don't descend further
376 376 elif followsym:
377 377 newdirs = []
378 378 for d in dirs:
379 379 fname = os.path.join(root, d)
380 380 if adddir(seen_dirs, fname):
381 381 if os.path.islink(fname):
382 382 for hgname in walkrepos(fname, True, seen_dirs):
383 383 yield hgname
384 384 else:
385 385 newdirs.append(d)
386 386 dirs[:] = newdirs
387 387
388 388 def osrcpath():
389 389 '''return default os-specific hgrc search path'''
390 390 path = systemrcpath()
391 391 path.extend(userrcpath())
392 392 path = [os.path.normpath(f) for f in path]
393 393 return path
394 394
395 395 _rcpath = None
396 396
397 397 def rcpath():
398 398 '''return hgrc search path. if env var HGRCPATH is set, use it.
399 399 for each item in path, if directory, use files ending in .rc,
400 400 else use item.
401 401 make HGRCPATH empty to only look in .hg/hgrc of current repo.
402 402 if no HGRCPATH, use default os-specific path.'''
403 403 global _rcpath
404 404 if _rcpath is None:
405 405 if 'HGRCPATH' in os.environ:
406 406 _rcpath = []
407 407 for p in os.environ['HGRCPATH'].split(os.pathsep):
408 408 if not p:
409 409 continue
410 410 p = util.expandpath(p)
411 411 if os.path.isdir(p):
412 412 for f, kind in osutil.listdir(p):
413 413 if f.endswith('.rc'):
414 414 _rcpath.append(os.path.join(p, f))
415 415 else:
416 416 _rcpath.append(p)
417 417 else:
418 418 _rcpath = osrcpath()
419 419 return _rcpath
420 420
421 421 if os.name != 'nt':
422 422
423 423 def rcfiles(path):
424 424 rcs = [os.path.join(path, 'hgrc')]
425 425 rcdir = os.path.join(path, 'hgrc.d')
426 426 try:
427 427 rcs.extend([os.path.join(rcdir, f)
428 428 for f, kind in osutil.listdir(rcdir)
429 429 if f.endswith(".rc")])
430 430 except OSError:
431 431 pass
432 432 return rcs
433 433
434 434 def systemrcpath():
435 435 path = []
436 436 # old mod_python does not set sys.argv
437 437 if len(getattr(sys, 'argv', [])) > 0:
438 438 p = os.path.dirname(os.path.dirname(sys.argv[0]))
439 439 path.extend(rcfiles(os.path.join(p, 'etc/mercurial')))
440 440 path.extend(rcfiles('/etc/mercurial'))
441 441 return path
442 442
443 443 def userrcpath():
444 444 return [os.path.expanduser('~/.hgrc')]
445 445
446 446 else:
447 447
448 448 _HKEY_LOCAL_MACHINE = 0x80000002L
449 449
450 450 def systemrcpath():
451 451 '''return default os-specific hgrc search path'''
452 452 rcpath = []
453 453 filename = util.executablepath()
454 454 # Use mercurial.ini found in directory with hg.exe
455 455 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
456 456 if os.path.isfile(progrc):
457 457 rcpath.append(progrc)
458 458 return rcpath
459 459 # Use hgrc.d found in directory with hg.exe
460 460 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
461 461 if os.path.isdir(progrcd):
462 462 for f, kind in osutil.listdir(progrcd):
463 463 if f.endswith('.rc'):
464 464 rcpath.append(os.path.join(progrcd, f))
465 465 return rcpath
466 466 # else look for a system rcpath in the registry
467 467 value = util.lookupreg('SOFTWARE\\Mercurial', None,
468 468 _HKEY_LOCAL_MACHINE)
469 469 if not isinstance(value, str) or not value:
470 470 return rcpath
471 471 value = util.localpath(value)
472 472 for p in value.split(os.pathsep):
473 473 if p.lower().endswith('mercurial.ini'):
474 474 rcpath.append(p)
475 475 elif os.path.isdir(p):
476 476 for f, kind in osutil.listdir(p):
477 477 if f.endswith('.rc'):
478 478 rcpath.append(os.path.join(p, f))
479 479 return rcpath
480 480
481 481 def userrcpath():
482 482 '''return os-specific hgrc search path to the user dir'''
483 483 home = os.path.expanduser('~')
484 484 path = [os.path.join(home, 'mercurial.ini'),
485 485 os.path.join(home, '.hgrc')]
486 486 userprofile = os.environ.get('USERPROFILE')
487 487 if userprofile:
488 488 path.append(os.path.join(userprofile, 'mercurial.ini'))
489 489 path.append(os.path.join(userprofile, '.hgrc'))
490 490 return path
491 491
492 492 def revsingle(repo, revspec, default='.'):
493 493 if not revspec:
494 494 return repo[default]
495 495
496 496 l = revrange(repo, [revspec])
497 497 if len(l) < 1:
498 498 raise util.Abort(_('empty revision set'))
499 499 return repo[l[-1]]
500 500
501 501 def revpair(repo, revs):
502 502 if not revs:
503 503 return repo.dirstate.p1(), None
504 504
505 505 l = revrange(repo, revs)
506 506
507 507 if len(l) == 0:
508 508 return repo.dirstate.p1(), None
509 509
510 510 if len(l) == 1:
511 511 return repo.lookup(l[0]), None
512 512
513 513 return repo.lookup(l[0]), repo.lookup(l[-1])
514 514
515 515 _revrangesep = ':'
516 516
517 517 def revrange(repo, revs):
518 518 """Yield revision as strings from a list of revision specifications."""
519 519
520 520 def revfix(repo, val, defval):
521 521 if not val and val != 0 and defval is not None:
522 522 return defval
523 523 return repo.changelog.rev(repo.lookup(val))
524 524
525 525 seen, l = set(), []
526 526 for spec in revs:
527 527 # attempt to parse old-style ranges first to deal with
528 528 # things like old-tag which contain query metacharacters
529 529 try:
530 530 if isinstance(spec, int):
531 531 seen.add(spec)
532 532 l.append(spec)
533 533 continue
534 534
535 535 if _revrangesep in spec:
536 536 start, end = spec.split(_revrangesep, 1)
537 537 start = revfix(repo, start, 0)
538 538 end = revfix(repo, end, len(repo) - 1)
539 539 step = start > end and -1 or 1
540 540 for rev in xrange(start, end + step, step):
541 541 if rev in seen:
542 542 continue
543 543 seen.add(rev)
544 544 l.append(rev)
545 545 continue
546 546 elif spec and spec in repo: # single unquoted rev
547 547 rev = revfix(repo, spec, None)
548 548 if rev in seen:
549 549 continue
550 550 seen.add(rev)
551 551 l.append(rev)
552 552 continue
553 553 except error.RepoLookupError:
554 554 pass
555 555
556 556 # fall through to new-style queries if old-style fails
557 557 m = revset.match(repo.ui, spec)
558 558 for r in m(repo, range(len(repo))):
559 559 if r not in seen:
560 560 l.append(r)
561 561 seen.update(l)
562 562
563 563 return l
564 564
565 565 def expandpats(pats):
566 566 if not util.expandglobs:
567 567 return list(pats)
568 568 ret = []
569 569 for p in pats:
570 570 kind, name = matchmod._patsplit(p, None)
571 571 if kind is None:
572 572 try:
573 573 globbed = glob.glob(name)
574 574 except re.error:
575 575 globbed = [name]
576 576 if globbed:
577 577 ret.extend(globbed)
578 578 continue
579 579 ret.append(p)
580 580 return ret
581 581
582 582 def match(ctx, pats=[], opts={}, globbed=False, default='relpath'):
583 583 if pats == ("",):
584 584 pats = []
585 585 if not globbed and default == 'relpath':
586 586 pats = expandpats(pats or [])
587 587
588 588 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
589 589 default)
590 590 def badfn(f, msg):
591 591 ctx._repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
592 592 m.bad = badfn
593 593 return m
594 594
595 595 def matchall(repo):
596 596 return matchmod.always(repo.root, repo.getcwd())
597 597
598 598 def matchfiles(repo, files):
599 599 return matchmod.exact(repo.root, repo.getcwd(), files)
600 600
601 601 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
602 602 if dry_run is None:
603 603 dry_run = opts.get('dry_run')
604 604 if similarity is None:
605 605 similarity = float(opts.get('similarity') or 0)
606 606 # we'd use status here, except handling of symlinks and ignore is tricky
607 607 added, unknown, deleted, removed = [], [], [], []
608 608 audit_path = pathauditor(repo.root)
609 609 m = match(repo[None], pats, opts)
610 610 for abs in repo.walk(m):
611 611 target = repo.wjoin(abs)
612 612 good = True
613 613 try:
614 614 audit_path(abs)
615 615 except (OSError, util.Abort):
616 616 good = False
617 617 rel = m.rel(abs)
618 618 exact = m.exact(abs)
619 619 if good and abs not in repo.dirstate:
620 620 unknown.append(abs)
621 621 if repo.ui.verbose or not exact:
622 622 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
623 623 elif repo.dirstate[abs] != 'r' and (not good or not os.path.lexists(target)
624 624 or (os.path.isdir(target) and not os.path.islink(target))):
625 625 deleted.append(abs)
626 626 if repo.ui.verbose or not exact:
627 627 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
628 628 # for finding renames
629 629 elif repo.dirstate[abs] == 'r':
630 630 removed.append(abs)
631 631 elif repo.dirstate[abs] == 'a':
632 632 added.append(abs)
633 633 copies = {}
634 634 if similarity > 0:
635 635 for old, new, score in similar.findrenames(repo,
636 636 added + unknown, removed + deleted, similarity):
637 637 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
638 638 repo.ui.status(_('recording removal of %s as rename to %s '
639 639 '(%d%% similar)\n') %
640 640 (m.rel(old), m.rel(new), score * 100))
641 641 copies[new] = old
642 642
643 643 if not dry_run:
644 644 wctx = repo[None]
645 645 wlock = repo.wlock()
646 646 try:
647 647 wctx.forget(deleted)
648 648 wctx.add(unknown)
649 649 for new, old in copies.iteritems():
650 650 wctx.copy(old, new)
651 651 finally:
652 652 wlock.release()
653 653
654 654 def updatedir(ui, repo, patches, similarity=0):
655 655 '''Update dirstate after patch application according to metadata'''
656 656 if not patches:
657 657 return []
658 658 copies = []
659 659 removes = set()
660 660 cfiles = patches.keys()
661 661 cwd = repo.getcwd()
662 662 if cwd:
663 663 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
664 664 for f in patches:
665 665 gp = patches[f]
666 666 if not gp:
667 667 continue
668 668 if gp.op == 'RENAME':
669 669 copies.append((gp.oldpath, gp.path))
670 670 removes.add(gp.oldpath)
671 671 elif gp.op == 'COPY':
672 672 copies.append((gp.oldpath, gp.path))
673 673 elif gp.op == 'DELETE':
674 674 removes.add(gp.path)
675 675
676 676 wctx = repo[None]
677 677 for src, dst in copies:
678 678 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
679 679 if (not similarity) and removes:
680 680 wctx.remove(sorted(removes), True)
681 681
682 682 for f in patches:
683 683 gp = patches[f]
684 684 if gp and gp.mode:
685 685 islink, isexec = gp.mode
686 686 dst = repo.wjoin(gp.path)
687 687 # patch won't create empty files
688 688 if gp.op == 'ADD' and not os.path.lexists(dst):
689 689 flags = (isexec and 'x' or '') + (islink and 'l' or '')
690 690 repo.wwrite(gp.path, '', flags)
691 691 util.setflags(dst, islink, isexec)
692 692 addremove(repo, cfiles, similarity=similarity)
693 693 files = patches.keys()
694 694 files.extend([r for r in removes if r not in files])
695 695 return sorted(files)
696 696
697 697 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
698 698 """Update the dirstate to reflect the intent of copying src to dst. For
699 699 different reasons it might not end with dst being marked as copied from src.
700 700 """
701 701 origsrc = repo.dirstate.copied(src) or src
702 702 if dst == origsrc: # copying back a copy?
703 703 if repo.dirstate[dst] not in 'mn' and not dryrun:
704 704 repo.dirstate.normallookup(dst)
705 705 else:
706 706 if repo.dirstate[origsrc] == 'a' and origsrc == src:
707 707 if not ui.quiet:
708 708 ui.warn(_("%s has not been committed yet, so no copy "
709 709 "data will be stored for %s.\n")
710 710 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
711 711 if repo.dirstate[dst] in '?r' and not dryrun:
712 712 wctx.add([dst])
713 713 elif not dryrun:
714 714 wctx.copy(origsrc, dst)
715 715
716 716 def readrequires(opener, supported):
717 717 '''Reads and parses .hg/requires and checks if all entries found
718 718 are in the list of supported features.'''
719 719 requirements = set(opener.read("requires").splitlines())
720 720 missings = []
721 721 for r in requirements:
722 722 if r not in supported:
723 723 if not r or not r[0].isalnum():
724 724 raise error.RequirementError(_(".hg/requires file is corrupt"))
725 725 missings.append(r)
726 726 missings.sort()
727 727 if missings:
728 728 raise error.RequirementError(_("unknown repository format: "
729 729 "requires features '%s' (upgrade Mercurial)") % "', '".join(missings))
730 730 return requirements
731 731
732 732 class filecacheentry(object):
733 733 def __init__(self, path):
734 734 self.path = path
735 735 self.cachestat = filecacheentry.stat(self.path)
736 736
737 737 if self.cachestat:
738 738 self._cacheable = self.cachestat.cacheable()
739 739 else:
740 740 # None means we don't know yet
741 741 self._cacheable = None
742 742
743 743 def refresh(self):
744 744 if self.cacheable():
745 745 self.cachestat = filecacheentry.stat(self.path)
746 746
747 747 def cacheable(self):
748 748 if self._cacheable is not None:
749 749 return self._cacheable
750 750
751 751 # we don't know yet, assume it is for now
752 752 return True
753 753
754 754 def changed(self):
755 755 # no point in going further if we can't cache it
756 756 if not self.cacheable():
757 757 return True
758 758
759 759 newstat = filecacheentry.stat(self.path)
760 760
761 761 # we may not know if it's cacheable yet, check again now
762 762 if newstat and self._cacheable is None:
763 763 self._cacheable = newstat.cacheable()
764 764
765 765 # check again
766 766 if not self._cacheable:
767 767 return True
768 768
769 769 if self.cachestat != newstat:
770 770 self.cachestat = newstat
771 771 return True
772 772 else:
773 773 return False
774 774
775 775 @staticmethod
776 776 def stat(path):
777 777 try:
778 778 return util.cachestat(path)
779 779 except OSError, e:
780 780 if e.errno != errno.ENOENT:
781 781 raise
782 782
783 783 class filecache(object):
784 784 '''A property like decorator that tracks a file under .hg/ for updates.
785 785
786 786 Records stat info when called in _filecache.
787 787
788 788 On subsequent calls, compares old stat info with new info, and recreates
789 789 the object when needed, updating the new stat info in _filecache.
790 790
791 791 Mercurial either atomic renames or appends for files under .hg,
792 792 so to ensure the cache is reliable we need the filesystem to be able
793 793 to tell us if a file has been replaced. If it can't, we fallback to
794 794 recreating the object on every call (essentially the same behaviour as
795 795 propertycache).'''
796 796 def __init__(self, path, instore=False):
797 797 self.path = path
798 798 self.instore = instore
799 799
800 800 def __call__(self, func):
801 801 self.func = func
802 802 self.name = func.__name__
803 803 return self
804 804
805 805 def __get__(self, obj, type=None):
806 # do we need to check if the file changed?
807 if self.name in obj.__dict__:
808 return obj.__dict__[self.name]
809
806 810 entry = obj._filecache.get(self.name)
807 811
808 812 if entry:
809 813 if entry.changed():
810 814 entry.obj = self.func(obj)
811 815 else:
812 816 path = self.instore and obj.sjoin(self.path) or obj.join(self.path)
813 817
814 818 # We stat -before- creating the object so our cache doesn't lie if
815 819 # a writer modified between the time we read and stat
816 820 entry = filecacheentry(path)
817 821 entry.obj = self.func(obj)
818 822
819 823 obj._filecache[self.name] = entry
820 824
821 setattr(obj, self.name, entry.obj)
825 obj.__dict__[self.name] = entry.obj
822 826 return entry.obj
827
828 def __set__(self, obj, value):
829 if self.name in obj._filecache:
830 obj._filecache[self.name].obj = value # update cached copy
831 obj.__dict__[self.name] = value # update copy returned by obj.x
832
833 def __delete__(self, obj):
834 try:
835 del obj.__dict__[self.name]
836 except KeyError:
837 raise AttributeError, self.name
@@ -1,139 +1,139
1 1 # statichttprepo.py - simple http repository class for mercurial
2 2 #
3 3 # This provides read-only repo access to repositories exported via static http
4 4 #
5 5 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from i18n import _
11 11 import changelog, byterange, url, error
12 12 import localrepo, manifest, util, scmutil, store
13 13 import urllib, urllib2, errno
14 14
15 15 class httprangereader(object):
16 16 def __init__(self, url, opener):
17 17 # we assume opener has HTTPRangeHandler
18 18 self.url = url
19 19 self.pos = 0
20 20 self.opener = opener
21 21 self.name = url
22 22 def seek(self, pos):
23 23 self.pos = pos
24 24 def read(self, bytes=None):
25 25 req = urllib2.Request(self.url)
26 26 end = ''
27 27 if bytes:
28 28 end = self.pos + bytes - 1
29 29 req.add_header('Range', 'bytes=%d-%s' % (self.pos, end))
30 30
31 31 try:
32 32 f = self.opener.open(req)
33 33 data = f.read()
34 34 # Python 2.6+ defines a getcode() function, and 2.4 and
35 35 # 2.5 appear to always have an undocumented code attribute
36 36 # set. If we can't read either of those, fall back to 206
37 37 # and hope for the best.
38 38 code = getattr(f, 'getcode', lambda : getattr(f, 'code', 206))()
39 39 except urllib2.HTTPError, inst:
40 40 num = inst.code == 404 and errno.ENOENT or None
41 41 raise IOError(num, inst)
42 42 except urllib2.URLError, inst:
43 43 raise IOError(None, inst.reason[1])
44 44
45 45 if code == 200:
46 46 # HTTPRangeHandler does nothing if remote does not support
47 47 # Range headers and returns the full entity. Let's slice it.
48 48 if bytes:
49 49 data = data[self.pos:self.pos + bytes]
50 50 else:
51 51 data = data[self.pos:]
52 52 elif bytes:
53 53 data = data[:bytes]
54 54 self.pos += len(data)
55 55 return data
56 56 def __iter__(self):
57 57 return iter(self.read().splitlines(1))
58 58 def close(self):
59 59 pass
60 60
61 61 def build_opener(ui, authinfo):
62 62 # urllib cannot handle URLs with embedded user or passwd
63 63 urlopener = url.opener(ui, authinfo)
64 64 urlopener.add_handler(byterange.HTTPRangeHandler())
65 65
66 66 class statichttpopener(scmutil.abstractopener):
67 67 def __init__(self, base):
68 68 self.base = base
69 69
70 70 def __call__(self, path, mode="r", atomictemp=None):
71 71 if mode not in ('r', 'rb'):
72 72 raise IOError('Permission denied')
73 73 f = "/".join((self.base, urllib.quote(path)))
74 74 return httprangereader(f, urlopener)
75 75
76 76 return statichttpopener
77 77
78 78 class statichttprepository(localrepo.localrepository):
79 79 def __init__(self, ui, path):
80 80 self._url = path
81 81 self.ui = ui
82 82
83 83 self.root = path
84 84 u = util.url(path.rstrip('/') + "/.hg")
85 85 self.path, authinfo = u.authinfo()
86 86
87 87 opener = build_opener(ui, authinfo)
88 88 self.opener = opener(self.path)
89 89 self._phasedefaults = []
90 90
91 91 try:
92 92 requirements = scmutil.readrequires(self.opener, self.supported)
93 93 except IOError, inst:
94 94 if inst.errno != errno.ENOENT:
95 95 raise
96 96 requirements = set()
97 97
98 98 # check if it is a non-empty old-style repository
99 99 try:
100 100 fp = self.opener("00changelog.i")
101 101 fp.read(1)
102 102 fp.close()
103 103 except IOError, inst:
104 104 if inst.errno != errno.ENOENT:
105 105 raise
106 106 # we do not care about empty old-style repositories here
107 107 msg = _("'%s' does not appear to be an hg repository") % path
108 108 raise error.RepoError(msg)
109 109
110 110 # setup store
111 111 self.store = store.store(requirements, self.path, opener)
112 112 self.spath = self.store.path
113 113 self.sopener = self.store.opener
114 114 self.sjoin = self.store.join
115 self._filecache = {}
115 116
116 117 self.manifest = manifest.manifest(self.sopener)
117 118 self.changelog = changelog.changelog(self.sopener)
118 119 self._tags = None
119 120 self.nodetagscache = None
120 121 self._branchcache = None
121 122 self._branchcachetip = None
122 123 self.encodepats = None
123 124 self.decodepats = None
124 125 self.capabilities.difference_update(["pushkey"])
125 self._filecache = {}
126 126
127 127 def url(self):
128 128 return self._url
129 129
130 130 def local(self):
131 131 return False
132 132
133 133 def lock(self, wait=True):
134 134 raise util.Abort(_('cannot lock static-http repository'))
135 135
136 136 def instance(ui, path, create):
137 137 if create:
138 138 raise util.Abort(_('cannot create new static-http repository'))
139 139 return statichttprepository(ui, path[7:])
@@ -1,214 +1,221
1 1 import sys, os, struct, subprocess, cStringIO, re, shutil
2 2
3 3 def connect(path=None):
4 4 cmdline = ['hg', 'serve', '--cmdserver', 'pipe']
5 5 if path:
6 6 cmdline += ['-R', path]
7 7
8 8 server = subprocess.Popen(cmdline, stdin=subprocess.PIPE,
9 9 stdout=subprocess.PIPE)
10 10
11 11 return server
12 12
13 13 def writeblock(server, data):
14 14 server.stdin.write(struct.pack('>I', len(data)))
15 15 server.stdin.write(data)
16 16 server.stdin.flush()
17 17
18 18 def readchannel(server):
19 19 data = server.stdout.read(5)
20 20 if not data:
21 21 raise EOFError()
22 22 channel, length = struct.unpack('>cI', data)
23 23 if channel in 'IL':
24 24 return channel, length
25 25 else:
26 26 return channel, server.stdout.read(length)
27 27
28 28 def runcommand(server, args, output=sys.stdout, error=sys.stderr, input=None):
29 29 print ' runcommand', ' '.join(args)
30 30 server.stdin.write('runcommand\n')
31 31 writeblock(server, '\0'.join(args))
32 32
33 33 if not input:
34 34 input = cStringIO.StringIO()
35 35
36 36 while True:
37 37 ch, data = readchannel(server)
38 38 if ch == 'o':
39 39 output.write(data)
40 40 output.flush()
41 41 elif ch == 'e':
42 42 error.write(data)
43 43 error.flush()
44 44 elif ch == 'I':
45 45 writeblock(server, input.read(data))
46 46 elif ch == 'L':
47 47 writeblock(server, input.readline(data))
48 48 elif ch == 'r':
49 49 return struct.unpack('>i', data)[0]
50 50 else:
51 51 print "unexpected channel %c: %r" % (ch, data)
52 52 if ch.isupper():
53 53 return
54 54
55 55 def check(func, repopath=None):
56 56 print
57 57 print 'testing %s:' % func.__name__
58 58 print
59 59 server = connect(repopath)
60 60 try:
61 61 return func(server)
62 62 finally:
63 63 server.stdin.close()
64 64 server.wait()
65 65
66 66 def unknowncommand(server):
67 67 server.stdin.write('unknowncommand\n')
68 68
69 69 def hellomessage(server):
70 70 ch, data = readchannel(server)
71 71 # escaping python tests output not supported
72 72 print '%c, %r' % (ch, re.sub('encoding: [a-zA-Z0-9-]+', 'encoding: ***', data))
73 73
74 74 # run an arbitrary command to make sure the next thing the server sends
75 75 # isn't part of the hello message
76 76 runcommand(server, ['id'])
77 77
78 78 def checkruncommand(server):
79 79 # hello block
80 80 readchannel(server)
81 81
82 82 # no args
83 83 runcommand(server, [])
84 84
85 85 # global options
86 86 runcommand(server, ['id', '--quiet'])
87 87
88 88 # make sure global options don't stick through requests
89 89 runcommand(server, ['id'])
90 90
91 91 # --config
92 92 runcommand(server, ['id', '--config', 'ui.quiet=True'])
93 93
94 94 # make sure --config doesn't stick
95 95 runcommand(server, ['id'])
96 96
97 97 def inputeof(server):
98 98 readchannel(server)
99 99 server.stdin.write('runcommand\n')
100 100 # close stdin while server is waiting for input
101 101 server.stdin.close()
102 102
103 103 # server exits with 1 if the pipe closed while reading the command
104 104 print 'server exit code =', server.wait()
105 105
106 106 def serverinput(server):
107 107 readchannel(server)
108 108
109 109 patch = """
110 110 # HG changeset patch
111 111 # User test
112 112 # Date 0 0
113 113 # Node ID c103a3dec114d882c98382d684d8af798d09d857
114 114 # Parent 0000000000000000000000000000000000000000
115 115 1
116 116
117 117 diff -r 000000000000 -r c103a3dec114 a
118 118 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
119 119 +++ b/a Thu Jan 01 00:00:00 1970 +0000
120 120 @@ -0,0 +1,1 @@
121 121 +1
122 122 """
123 123
124 124 runcommand(server, ['import', '-'], input=cStringIO.StringIO(patch))
125 125 runcommand(server, ['log'])
126 126
127 127 def cwd(server):
128 128 """ check that --cwd doesn't persist between requests """
129 129 readchannel(server)
130 130 os.mkdir('foo')
131 131 f = open('foo/bar', 'wb')
132 132 f.write('a')
133 133 f.close()
134 134 runcommand(server, ['--cwd', 'foo', 'st', 'bar'])
135 135 runcommand(server, ['st', 'foo/bar'])
136 136 os.remove('foo/bar')
137 137
138 138 def localhgrc(server):
139 139 """ check that local configs for the cached repo aren't inherited when -R
140 140 is used """
141 141 readchannel(server)
142 142
143 143 # the cached repo local hgrc contains ui.foo=bar, so showconfig should show it
144 144 runcommand(server, ['showconfig'])
145 145
146 146 # but not for this repo
147 147 runcommand(server, ['init', 'foo'])
148 148 runcommand(server, ['-R', 'foo', 'showconfig', 'ui', 'defaults'])
149 149 shutil.rmtree('foo')
150 150
151 151 def hook(**args):
152 152 print 'hook talking'
153 153 print 'now try to read something: %r' % sys.stdin.read()
154 154
155 155 def hookoutput(server):
156 156 readchannel(server)
157 157 runcommand(server, ['--config',
158 158 'hooks.pre-identify=python:test-commandserver.hook', 'id'],
159 159 input=cStringIO.StringIO('some input'))
160 160
161 161 def outsidechanges(server):
162 162 readchannel(server)
163 163 f = open('a', 'ab')
164 164 f.write('a\n')
165 165 f.close()
166 166 runcommand(server, ['status'])
167 167 os.system('hg ci -Am2')
168 168 runcommand(server, ['tip'])
169 169 runcommand(server, ['status'])
170 170
171 171 def bookmarks(server):
172 172 readchannel(server)
173 173 runcommand(server, ['bookmarks'])
174 174
175 175 # changes .hg/bookmarks
176 176 os.system('hg bookmark -i bm1')
177 177 os.system('hg bookmark -i bm2')
178 178 runcommand(server, ['bookmarks'])
179 179
180 180 # changes .hg/bookmarks.current
181 181 os.system('hg upd bm1 -q')
182 182 runcommand(server, ['bookmarks'])
183 183
184 runcommand(server, ['bookmarks', 'bm3'])
185 f = open('a', 'ab')
186 f.write('a\n')
187 f.close()
188 runcommand(server, ['commit', '-Amm'])
189 runcommand(server, ['bookmarks'])
190
184 191 def tagscache(server):
185 192 readchannel(server)
186 193 runcommand(server, ['id', '-t', '-r', '0'])
187 194 os.system('hg tag -r 0 foo')
188 195 runcommand(server, ['id', '-t', '-r', '0'])
189 196
190 197 def setphase(server):
191 198 readchannel(server)
192 199 runcommand(server, ['phase', '-r', '.'])
193 200 os.system('hg phase -r . -p')
194 201 runcommand(server, ['phase', '-r', '.'])
195 202
196 203 if __name__ == '__main__':
197 204 os.system('hg init')
198 205
199 206 check(hellomessage)
200 207 check(unknowncommand)
201 208 check(checkruncommand)
202 209 check(inputeof)
203 210 check(serverinput)
204 211 check(cwd)
205 212
206 213 hgrc = open('.hg/hgrc', 'a')
207 214 hgrc.write('[ui]\nfoo=bar\n')
208 215 hgrc.close()
209 216 check(localhgrc)
210 217 check(hookoutput)
211 218 check(outsidechanges)
212 219 check(bookmarks)
213 220 check(tagscache)
214 221 check(setphase)
@@ -1,130 +1,136
1 1
2 2 testing hellomessage:
3 3
4 4 o, 'capabilities: getencoding runcommand\nencoding: ***'
5 5 runcommand id
6 6 000000000000 tip
7 7 abort: unknown command unknowncommand
8 8
9 9 testing unknowncommand:
10 10
11 11
12 12 testing checkruncommand:
13 13
14 14 runcommand
15 15 Mercurial Distributed SCM
16 16
17 17 basic commands:
18 18
19 19 add add the specified files on the next commit
20 20 annotate show changeset information by line for each file
21 21 clone make a copy of an existing repository
22 22 commit commit the specified files or all outstanding changes
23 23 diff diff repository (or selected files)
24 24 export dump the header and diffs for one or more changesets
25 25 forget forget the specified files on the next commit
26 26 init create a new repository in the given directory
27 27 log show revision history of entire repository or files
28 28 merge merge working directory with another revision
29 29 phase set or show the current phase name
30 30 pull pull changes from the specified source
31 31 push push changes to the specified destination
32 32 remove remove the specified files on the next commit
33 33 serve start stand-alone webserver
34 34 status show changed files in the working directory
35 35 summary summarize working directory state
36 36 update update working directory (or switch revisions)
37 37
38 38 use "hg help" for the full list of commands or "hg -v" for details
39 39 runcommand id --quiet
40 40 000000000000
41 41 runcommand id
42 42 000000000000 tip
43 43 runcommand id --config ui.quiet=True
44 44 000000000000
45 45 runcommand id
46 46 000000000000 tip
47 47
48 48 testing inputeof:
49 49
50 50 server exit code = 1
51 51
52 52 testing serverinput:
53 53
54 54 runcommand import -
55 55 applying patch from stdin
56 56 runcommand log
57 57 changeset: 0:eff892de26ec
58 58 tag: tip
59 59 user: test
60 60 date: Thu Jan 01 00:00:00 1970 +0000
61 61 summary: 1
62 62
63 63
64 64 testing cwd:
65 65
66 66 runcommand --cwd foo st bar
67 67 ? bar
68 68 runcommand st foo/bar
69 69 ? foo/bar
70 70
71 71 testing localhgrc:
72 72
73 73 runcommand showconfig
74 74 bundle.mainreporoot=$TESTTMP
75 75 defaults.backout=-d "0 0"
76 76 defaults.commit=-d "0 0"
77 77 defaults.tag=-d "0 0"
78 78 ui.slash=True
79 79 ui.foo=bar
80 80 runcommand init foo
81 81 runcommand -R foo showconfig ui defaults
82 82 defaults.backout=-d "0 0"
83 83 defaults.commit=-d "0 0"
84 84 defaults.tag=-d "0 0"
85 85 ui.slash=True
86 86
87 87 testing hookoutput:
88 88
89 89 runcommand --config hooks.pre-identify=python:test-commandserver.hook id
90 90 hook talking
91 91 now try to read something: 'some input'
92 92 eff892de26ec tip
93 93
94 94 testing outsidechanges:
95 95
96 96 runcommand status
97 97 M a
98 98 runcommand tip
99 99 changeset: 1:d3a0a68be6de
100 100 tag: tip
101 101 user: test
102 102 date: Thu Jan 01 00:00:00 1970 +0000
103 103 summary: 2
104 104
105 105 runcommand status
106 106
107 107 testing bookmarks:
108 108
109 109 runcommand bookmarks
110 110 no bookmarks set
111 111 runcommand bookmarks
112 112 bm1 1:d3a0a68be6de
113 113 bm2 1:d3a0a68be6de
114 114 runcommand bookmarks
115 115 * bm1 1:d3a0a68be6de
116 116 bm2 1:d3a0a68be6de
117 runcommand bookmarks bm3
118 runcommand commit -Amm
119 runcommand bookmarks
120 bm1 1:d3a0a68be6de
121 bm2 1:d3a0a68be6de
122 * bm3 2:aef17e88f5f0
117 123
118 124 testing tagscache:
119 125
120 126 runcommand id -t -r 0
121 127
122 128 runcommand id -t -r 0
123 129 foo
124 130
125 131 testing setphase:
126 132
127 133 runcommand phase -r .
128 2: draft
134 3: draft
129 135 runcommand phase -r .
130 2: public
136 3: public
General Comments 0
You need to be logged in to leave comments. Login now