##// END OF EJS Templates
narrow: extract part of narrowspec backup to core...
Martin von Zweigbergk -
r38872:fed6fe85 default
parent child Browse files
Show More
@@ -1,99 +1,94 b''
1 1 # narrowdirstate.py - extensions to mercurial dirstate to support narrow clones
2 2 #
3 3 # Copyright 2017 Google, Inc.
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 __future__ import absolute_import
9 9
10 10 from mercurial.i18n import _
11 11 from mercurial import (
12 12 error,
13 13 match as matchmod,
14 14 narrowspec,
15 util as hgutil,
16 15 )
17 16
18 17 def wrapdirstate(repo, dirstate):
19 18 """Add narrow spec dirstate ignore, block changes outside narrow spec."""
20 19
21 20 def _editfunc(fn):
22 21 def _wrapper(self, *args):
23 22 dirstate = repo.dirstate
24 23 narrowmatch = repo.narrowmatch()
25 24 for f in args:
26 25 if f is not None and not narrowmatch(f) and f not in dirstate:
27 26 raise error.Abort(_("cannot track '%s' - it is outside " +
28 27 "the narrow clone") % f)
29 28 return fn(self, *args)
30 29 return _wrapper
31 30
32 31 def _narrowbackupname(backupname):
33 32 assert 'dirstate' in backupname
34 33 return backupname.replace('dirstate', narrowspec.FILENAME)
35 34
36 35 class narrowdirstate(dirstate.__class__):
37 36 def walk(self, match, subrepos, unknown, ignored, full=True,
38 37 narrowonly=True):
39 38 if narrowonly:
40 39 # hack to not exclude explicitly-specified paths so that they
41 40 # can be warned later on e.g. dirstate.add()
42 41 em = matchmod.exact(match._root, match._cwd, match.files())
43 42 nm = matchmod.unionmatcher([repo.narrowmatch(), em])
44 43 match = matchmod.intersectmatchers(match, nm)
45 44 return super(narrowdirstate, self).walk(match, subrepos, unknown,
46 45 ignored, full)
47 46
48 47 # Prevent adding/editing/copying/deleting files that are outside the
49 48 # sparse checkout
50 49 @_editfunc
51 50 def normal(self, *args):
52 51 return super(narrowdirstate, self).normal(*args)
53 52
54 53 @_editfunc
55 54 def add(self, *args):
56 55 return super(narrowdirstate, self).add(*args)
57 56
58 57 @_editfunc
59 58 def normallookup(self, *args):
60 59 return super(narrowdirstate, self).normallookup(*args)
61 60
62 61 @_editfunc
63 62 def copy(self, *args):
64 63 return super(narrowdirstate, self).copy(*args)
65 64
66 65 @_editfunc
67 66 def remove(self, *args):
68 67 return super(narrowdirstate, self).remove(*args)
69 68
70 69 @_editfunc
71 70 def merge(self, *args):
72 71 return super(narrowdirstate, self).merge(*args)
73 72
74 73 def rebuild(self, parent, allfiles, changedfiles=None):
75 74 if changedfiles is None:
76 75 # Rebuilding entire dirstate, let's filter allfiles to match the
77 76 # narrowspec.
78 77 allfiles = [f for f in allfiles if repo.narrowmatch()(f)]
79 78 super(narrowdirstate, self).rebuild(parent, allfiles, changedfiles)
80 79
81 80 def restorebackup(self, tr, backupname):
82 self._opener.rename(_narrowbackupname(backupname),
83 narrowspec.FILENAME, checkambig=True)
81 narrowspec.restorebackup(self._opener,
82 _narrowbackupname(backupname))
84 83 super(narrowdirstate, self).restorebackup(tr, backupname)
85 84
86 85 def savebackup(self, tr, backupname):
87 86 super(narrowdirstate, self).savebackup(tr, backupname)
88
89 narrowbackupname = _narrowbackupname(backupname)
90 self._opener.tryunlink(narrowbackupname)
91 hgutil.copyfile(self._opener.join(narrowspec.FILENAME),
92 self._opener.join(narrowbackupname), hardlink=True)
87 narrowspec.savebackup(self._opener, _narrowbackupname(backupname))
93 88
94 89 def clearbackup(self, tr, backupname):
95 90 super(narrowdirstate, self).clearbackup(tr, backupname)
96 self._opener.unlink(_narrowbackupname(backupname))
91 narrowspec.clearbackup(self._opener, _narrowbackupname(backupname))
97 92
98 93 dirstate.__class__ = narrowdirstate
99 94 return dirstate
@@ -1,199 +1,209 b''
1 1 # narrowspec.py - methods for working with a narrow view of a repository
2 2 #
3 3 # Copyright 2017 Google, Inc.
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 __future__ import absolute_import
9 9
10 10 import errno
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 match as matchmod,
16 16 util,
17 17 )
18 18
19 19 FILENAME = 'narrowspec'
20 20
21 21 def _parsestoredpatterns(text):
22 22 """Parses the narrowspec format that's stored on disk."""
23 23 patlist = None
24 24 includepats = []
25 25 excludepats = []
26 26 for l in text.splitlines():
27 27 if l == '[includes]':
28 28 if patlist is None:
29 29 patlist = includepats
30 30 else:
31 31 raise error.Abort(_('narrowspec includes section must appear '
32 32 'at most once, before excludes'))
33 33 elif l == '[excludes]':
34 34 if patlist is not excludepats:
35 35 patlist = excludepats
36 36 else:
37 37 raise error.Abort(_('narrowspec excludes section must appear '
38 38 'at most once'))
39 39 else:
40 40 patlist.append(l)
41 41
42 42 return set(includepats), set(excludepats)
43 43
44 44 def parseserverpatterns(text):
45 45 """Parses the narrowspec format that's returned by the server."""
46 46 includepats = set()
47 47 excludepats = set()
48 48
49 49 # We get one entry per line, in the format "<key> <value>".
50 50 # It's OK for value to contain other spaces.
51 51 for kp in (l.split(' ', 1) for l in text.splitlines()):
52 52 if len(kp) != 2:
53 53 raise error.Abort(_('Invalid narrowspec pattern line: "%s"') % kp)
54 54 key = kp[0]
55 55 pat = kp[1]
56 56 if key == 'include':
57 57 includepats.add(pat)
58 58 elif key == 'exclude':
59 59 excludepats.add(pat)
60 60 else:
61 61 raise error.Abort(_('Invalid key "%s" in server response') % key)
62 62
63 63 return includepats, excludepats
64 64
65 65 def normalizesplitpattern(kind, pat):
66 66 """Returns the normalized version of a pattern and kind.
67 67
68 68 Returns a tuple with the normalized kind and normalized pattern.
69 69 """
70 70 pat = pat.rstrip('/')
71 71 _validatepattern(pat)
72 72 return kind, pat
73 73
74 74 def _numlines(s):
75 75 """Returns the number of lines in s, including ending empty lines."""
76 76 # We use splitlines because it is Unicode-friendly and thus Python 3
77 77 # compatible. However, it does not count empty lines at the end, so trick
78 78 # it by adding a character at the end.
79 79 return len((s + 'x').splitlines())
80 80
81 81 def _validatepattern(pat):
82 82 """Validates the pattern and aborts if it is invalid.
83 83
84 84 Patterns are stored in the narrowspec as newline-separated
85 85 POSIX-style bytestring paths. There's no escaping.
86 86 """
87 87
88 88 # We use newlines as separators in the narrowspec file, so don't allow them
89 89 # in patterns.
90 90 if _numlines(pat) > 1:
91 91 raise error.Abort(_('newlines are not allowed in narrowspec paths'))
92 92
93 93 components = pat.split('/')
94 94 if '.' in components or '..' in components:
95 95 raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
96 96
97 97 def normalizepattern(pattern, defaultkind='path'):
98 98 """Returns the normalized version of a text-format pattern.
99 99
100 100 If the pattern has no kind, the default will be added.
101 101 """
102 102 kind, pat = matchmod._patsplit(pattern, defaultkind)
103 103 return '%s:%s' % normalizesplitpattern(kind, pat)
104 104
105 105 def parsepatterns(pats):
106 106 """Parses a list of patterns into a typed pattern set."""
107 107 return set(normalizepattern(p) for p in pats)
108 108
109 109 def format(includes, excludes):
110 110 output = '[includes]\n'
111 111 for i in sorted(includes - excludes):
112 112 output += i + '\n'
113 113 output += '[excludes]\n'
114 114 for e in sorted(excludes):
115 115 output += e + '\n'
116 116 return output
117 117
118 118 def match(root, include=None, exclude=None):
119 119 if not include:
120 120 # Passing empty include and empty exclude to matchmod.match()
121 121 # gives a matcher that matches everything, so explicitly use
122 122 # the nevermatcher.
123 123 return matchmod.never(root, '')
124 124 return matchmod.match(root, '', [], include=include or [],
125 125 exclude=exclude or [])
126 126
127 127 def needsexpansion(includes):
128 128 return [i for i in includes if i.startswith('include:')]
129 129
130 130 def load(repo):
131 131 try:
132 132 spec = repo.vfs.read(FILENAME)
133 133 except IOError as e:
134 134 # Treat "narrowspec does not exist" the same as "narrowspec file exists
135 135 # and is empty".
136 136 if e.errno == errno.ENOENT:
137 137 # Without this the next call to load will use the cached
138 138 # non-existence of the file, which can cause some odd issues.
139 139 repo.invalidate(clearfilecache=True)
140 140 return set(), set()
141 141 raise
142 142 return _parsestoredpatterns(spec)
143 143
144 144 def save(repo, includepats, excludepats):
145 145 spec = format(includepats, excludepats)
146 146 repo.vfs.write(FILENAME, spec)
147 147
148 def savebackup(vfs, backupname):
149 vfs.tryunlink(backupname)
150 util.copyfile(vfs.join(FILENAME), vfs.join(backupname), hardlink=True)
151
152 def restorebackup(vfs, backupname):
153 vfs.rename(backupname, FILENAME, checkambig=True)
154
155 def clearbackup(vfs, backupname):
156 vfs.unlink(backupname)
157
148 158 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
149 159 r""" Restricts the patterns according to repo settings,
150 160 results in a logical AND operation
151 161
152 162 :param req_includes: requested includes
153 163 :param req_excludes: requested excludes
154 164 :param repo_includes: repo includes
155 165 :param repo_excludes: repo excludes
156 166 :return: include patterns, exclude patterns, and invalid include patterns.
157 167
158 168 >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
159 169 (set(['f1']), {}, [])
160 170 >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
161 171 (set(['f1']), {}, [])
162 172 >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
163 173 (set(['f1/fc1']), {}, [])
164 174 >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
165 175 ([], set(['path:.']), [])
166 176 >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
167 177 (set(['f2/fc2']), {}, [])
168 178 >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
169 179 ([], set(['path:.']), [])
170 180 >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
171 181 (set(['f1/$non_exitent_var']), {}, [])
172 182 """
173 183 res_excludes = set(req_excludes)
174 184 res_excludes.update(repo_excludes)
175 185 invalid_includes = []
176 186 if not req_includes:
177 187 res_includes = set(repo_includes)
178 188 elif 'path:.' not in repo_includes:
179 189 res_includes = []
180 190 for req_include in req_includes:
181 191 req_include = util.expandpath(util.normpath(req_include))
182 192 if req_include in repo_includes:
183 193 res_includes.append(req_include)
184 194 continue
185 195 valid = False
186 196 for repo_include in repo_includes:
187 197 if req_include.startswith(repo_include + '/'):
188 198 valid = True
189 199 res_includes.append(req_include)
190 200 break
191 201 if not valid:
192 202 invalid_includes.append(req_include)
193 203 if len(res_includes) == 0:
194 204 res_excludes = {'path:.'}
195 205 else:
196 206 res_includes = set(res_includes)
197 207 else:
198 208 res_includes = set(req_includes)
199 209 return res_includes, res_excludes, invalid_includes
General Comments 0
You need to be logged in to leave comments. Login now