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