Show More
@@ -0,0 +1,95 b'' | |||||
|
1 | import sys, os, subprocess | |||
|
2 | ||||
|
3 | try: | |||
|
4 | subprocess.check_call(['%s/hghave' % os.environ['TESTDIR'], 'cacheable']) | |||
|
5 | except subprocess.CalledProcessError: | |||
|
6 | sys.exit(80) | |||
|
7 | ||||
|
8 | from mercurial import util, scmutil, extensions | |||
|
9 | ||||
|
10 | filecache = scmutil.filecache | |||
|
11 | ||||
|
12 | class fakerepo(object): | |||
|
13 | def __init__(self): | |||
|
14 | self._filecache = {} | |||
|
15 | ||||
|
16 | def join(self, p): | |||
|
17 | return p | |||
|
18 | ||||
|
19 | def sjoin(self, p): | |||
|
20 | return p | |||
|
21 | ||||
|
22 | @filecache('x') | |||
|
23 | def cached(self): | |||
|
24 | print 'creating' | |||
|
25 | ||||
|
26 | def invalidate(self): | |||
|
27 | for k in self._filecache: | |||
|
28 | try: | |||
|
29 | delattr(self, k) | |||
|
30 | except AttributeError: | |||
|
31 | pass | |||
|
32 | ||||
|
33 | def basic(repo): | |||
|
34 | # file doesn't exist, calls function | |||
|
35 | repo.cached | |||
|
36 | ||||
|
37 | repo.invalidate() | |||
|
38 | # file still doesn't exist, uses cache | |||
|
39 | repo.cached | |||
|
40 | ||||
|
41 | # create empty file | |||
|
42 | f = open('x', 'w') | |||
|
43 | f.close() | |||
|
44 | repo.invalidate() | |||
|
45 | # should recreate the object | |||
|
46 | repo.cached | |||
|
47 | ||||
|
48 | f = open('x', 'w') | |||
|
49 | f.write('a') | |||
|
50 | f.close() | |||
|
51 | repo.invalidate() | |||
|
52 | # should recreate the object | |||
|
53 | repo.cached | |||
|
54 | ||||
|
55 | repo.invalidate() | |||
|
56 | # stats file again, nothing changed, reuses object | |||
|
57 | repo.cached | |||
|
58 | ||||
|
59 | # atomic replace file, size doesn't change | |||
|
60 | # hopefully st_mtime doesn't change as well so this doesn't use the cache | |||
|
61 | # because of inode change | |||
|
62 | f = scmutil.opener('.')('x', 'w', atomictemp=True) | |||
|
63 | f.write('b') | |||
|
64 | f.rename() | |||
|
65 | ||||
|
66 | repo.invalidate() | |||
|
67 | repo.cached | |||
|
68 | ||||
|
69 | def fakeuncacheable(): | |||
|
70 | def wrapcacheable(orig, *args, **kwargs): | |||
|
71 | return False | |||
|
72 | ||||
|
73 | def wrapinit(orig, *args, **kwargs): | |||
|
74 | pass | |||
|
75 | ||||
|
76 | originit = extensions.wrapfunction(util.cachestat, '__init__', wrapinit) | |||
|
77 | origcacheable = extensions.wrapfunction(util.cachestat, 'cacheable', wrapcacheable) | |||
|
78 | ||||
|
79 | try: | |||
|
80 | os.remove('x') | |||
|
81 | except: | |||
|
82 | pass | |||
|
83 | ||||
|
84 | basic(fakerepo()) | |||
|
85 | ||||
|
86 | util.cachestat.cacheable = origcacheable | |||
|
87 | util.cachestat.__init__ = originit | |||
|
88 | ||||
|
89 | print 'basic:' | |||
|
90 | ||||
|
91 | basic(fakerepo()) | |||
|
92 | ||||
|
93 | print 'fakeuncacheable:' | |||
|
94 | ||||
|
95 | fakeuncacheable() |
@@ -0,0 +1,15 b'' | |||||
|
1 | basic: | |||
|
2 | ||||
|
3 | creating | |||
|
4 | creating | |||
|
5 | creating | |||
|
6 | creating | |||
|
7 | ||||
|
8 | fakeuncacheable: | |||
|
9 | ||||
|
10 | creating | |||
|
11 | creating | |||
|
12 | creating | |||
|
13 | creating | |||
|
14 | creating | |||
|
15 | creating |
@@ -709,3 +709,95 b' def readrequires(opener, supported):' | |||||
709 | raise error.RequirementError(_("unknown repository format: " |
|
709 | raise error.RequirementError(_("unknown repository format: " | |
710 | "requires features '%s' (upgrade Mercurial)") % "', '".join(missings)) |
|
710 | "requires features '%s' (upgrade Mercurial)") % "', '".join(missings)) | |
711 | return requirements |
|
711 | return requirements | |
|
712 | ||||
|
713 | class filecacheentry(object): | |||
|
714 | def __init__(self, path): | |||
|
715 | self.path = path | |||
|
716 | self.cachestat = filecacheentry.stat(self.path) | |||
|
717 | ||||
|
718 | if self.cachestat: | |||
|
719 | self._cacheable = self.cachestat.cacheable() | |||
|
720 | else: | |||
|
721 | # None means we don't know yet | |||
|
722 | self._cacheable = None | |||
|
723 | ||||
|
724 | def refresh(self): | |||
|
725 | if self.cacheable(): | |||
|
726 | self.cachestat = filecacheentry.stat(self.path) | |||
|
727 | ||||
|
728 | def cacheable(self): | |||
|
729 | if self._cacheable is not None: | |||
|
730 | return self._cacheable | |||
|
731 | ||||
|
732 | # we don't know yet, assume it is for now | |||
|
733 | return True | |||
|
734 | ||||
|
735 | def changed(self): | |||
|
736 | # no point in going further if we can't cache it | |||
|
737 | if not self.cacheable(): | |||
|
738 | return True | |||
|
739 | ||||
|
740 | newstat = filecacheentry.stat(self.path) | |||
|
741 | ||||
|
742 | # we may not know if it's cacheable yet, check again now | |||
|
743 | if newstat and self._cacheable is None: | |||
|
744 | self._cacheable = newstat.cacheable() | |||
|
745 | ||||
|
746 | # check again | |||
|
747 | if not self._cacheable: | |||
|
748 | return True | |||
|
749 | ||||
|
750 | if self.cachestat != newstat: | |||
|
751 | self.cachestat = newstat | |||
|
752 | return True | |||
|
753 | else: | |||
|
754 | return False | |||
|
755 | ||||
|
756 | @staticmethod | |||
|
757 | def stat(path): | |||
|
758 | try: | |||
|
759 | return util.cachestat(path) | |||
|
760 | except OSError, e: | |||
|
761 | if e.errno != errno.ENOENT: | |||
|
762 | raise | |||
|
763 | ||||
|
764 | class filecache(object): | |||
|
765 | '''A property like decorator that tracks a file under .hg/ for updates. | |||
|
766 | ||||
|
767 | Records stat info when called in _filecache. | |||
|
768 | ||||
|
769 | On subsequent calls, compares old stat info with new info, and recreates | |||
|
770 | the object when needed, updating the new stat info in _filecache. | |||
|
771 | ||||
|
772 | Mercurial either atomic renames or appends for files under .hg, | |||
|
773 | so to ensure the cache is reliable we need the filesystem to be able | |||
|
774 | to tell us if a file has been replaced. If it can't, we fallback to | |||
|
775 | recreating the object on every call (essentially the same behaviour as | |||
|
776 | propertycache).''' | |||
|
777 | def __init__(self, path, instore=False): | |||
|
778 | self.path = path | |||
|
779 | self.instore = instore | |||
|
780 | ||||
|
781 | def __call__(self, func): | |||
|
782 | self.func = func | |||
|
783 | self.name = func.__name__ | |||
|
784 | return self | |||
|
785 | ||||
|
786 | def __get__(self, obj, type=None): | |||
|
787 | entry = obj._filecache.get(self.name) | |||
|
788 | ||||
|
789 | if entry: | |||
|
790 | if entry.changed(): | |||
|
791 | entry.obj = self.func(obj) | |||
|
792 | else: | |||
|
793 | path = self.instore and obj.sjoin(self.path) or obj.join(self.path) | |||
|
794 | ||||
|
795 | # We stat -before- creating the object so our cache doesn't lie if | |||
|
796 | # a writer modified between the time we read and stat | |||
|
797 | entry = filecacheentry(path) | |||
|
798 | entry.obj = self.func(obj) | |||
|
799 | ||||
|
800 | obj._filecache[self.name] = entry | |||
|
801 | ||||
|
802 | setattr(obj, self.name, entry.obj) | |||
|
803 | return entry.obj |
General Comments 0
You need to be logged in to leave comments.
Login now