Show More
@@ -0,0 +1,144 b'' | |||
|
1 | import os, errno, stat | |
|
2 | ||
|
3 | import util | |
|
4 | from i18n import _ | |
|
5 | ||
|
6 | class pathauditor(object): | |
|
7 | '''ensure that a filesystem path contains no banned components. | |
|
8 | the following properties of a path are checked: | |
|
9 | ||
|
10 | - ends with a directory separator | |
|
11 | - under top-level .hg | |
|
12 | - starts at the root of a windows drive | |
|
13 | - contains ".." | |
|
14 | - traverses a symlink (e.g. a/symlink_here/b) | |
|
15 | - inside a nested repository (a callback can be used to approve | |
|
16 | some nested repositories, e.g., subrepositories) | |
|
17 | ''' | |
|
18 | ||
|
19 | def __init__(self, root, callback=None): | |
|
20 | self.audited = set() | |
|
21 | self.auditeddir = set() | |
|
22 | self.root = root | |
|
23 | self.callback = callback | |
|
24 | if os.path.lexists(root) and not util.checkcase(root): | |
|
25 | self.normcase = util.normcase | |
|
26 | else: | |
|
27 | self.normcase = lambda x: x | |
|
28 | ||
|
29 | def __call__(self, path): | |
|
30 | '''Check the relative path. | |
|
31 | path may contain a pattern (e.g. foodir/**.txt)''' | |
|
32 | ||
|
33 | path = util.localpath(path) | |
|
34 | normpath = self.normcase(path) | |
|
35 | if normpath in self.audited: | |
|
36 | return | |
|
37 | # AIX ignores "/" at end of path, others raise EISDIR. | |
|
38 | if util.endswithsep(path): | |
|
39 | raise util.Abort(_("path ends in directory separator: %s") % path) | |
|
40 | parts = util.splitpath(path) | |
|
41 | if (os.path.splitdrive(path)[0] | |
|
42 | or parts[0].lower() in ('.hg', '.hg.', '') | |
|
43 | or os.pardir in parts): | |
|
44 | raise util.Abort(_("path contains illegal component: %s") % path) | |
|
45 | if '.hg' in path.lower(): | |
|
46 | lparts = [p.lower() for p in parts] | |
|
47 | for p in '.hg', '.hg.': | |
|
48 | if p in lparts[1:]: | |
|
49 | pos = lparts.index(p) | |
|
50 | base = os.path.join(*parts[:pos]) | |
|
51 | raise util.Abort(_("path '%s' is inside nested repo %r") | |
|
52 | % (path, base)) | |
|
53 | ||
|
54 | normparts = util.splitpath(normpath) | |
|
55 | assert len(parts) == len(normparts) | |
|
56 | ||
|
57 | parts.pop() | |
|
58 | normparts.pop() | |
|
59 | prefixes = [] | |
|
60 | while parts: | |
|
61 | prefix = os.sep.join(parts) | |
|
62 | normprefix = os.sep.join(normparts) | |
|
63 | if normprefix in self.auditeddir: | |
|
64 | break | |
|
65 | curpath = os.path.join(self.root, prefix) | |
|
66 | try: | |
|
67 | st = os.lstat(curpath) | |
|
68 | except OSError, err: | |
|
69 | # EINVAL can be raised as invalid path syntax under win32. | |
|
70 | # They must be ignored for patterns can be checked too. | |
|
71 | if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL): | |
|
72 | raise | |
|
73 | else: | |
|
74 | if stat.S_ISLNK(st.st_mode): | |
|
75 | raise util.Abort( | |
|
76 | _('path %r traverses symbolic link %r') | |
|
77 | % (path, prefix)) | |
|
78 | elif (stat.S_ISDIR(st.st_mode) and | |
|
79 | os.path.isdir(os.path.join(curpath, '.hg'))): | |
|
80 | if not self.callback or not self.callback(curpath): | |
|
81 | raise util.Abort(_("path '%s' is inside nested " | |
|
82 | "repo %r") | |
|
83 | % (path, prefix)) | |
|
84 | prefixes.append(normprefix) | |
|
85 | parts.pop() | |
|
86 | normparts.pop() | |
|
87 | ||
|
88 | self.audited.add(normpath) | |
|
89 | # only add prefixes to the cache after checking everything: we don't | |
|
90 | # want to add "foo/bar/baz" before checking if there's a "foo/.hg" | |
|
91 | self.auditeddir.update(prefixes) | |
|
92 | ||
|
93 | def check(self, path): | |
|
94 | try: | |
|
95 | self(path) | |
|
96 | return True | |
|
97 | except (OSError, util.Abort): | |
|
98 | return False | |
|
99 | ||
|
100 | def canonpath(root, cwd, myname, auditor=None): | |
|
101 | '''return the canonical path of myname, given cwd and root''' | |
|
102 | if util.endswithsep(root): | |
|
103 | rootsep = root | |
|
104 | else: | |
|
105 | rootsep = root + os.sep | |
|
106 | name = myname | |
|
107 | if not os.path.isabs(name): | |
|
108 | name = os.path.join(root, cwd, name) | |
|
109 | name = os.path.normpath(name) | |
|
110 | if auditor is None: | |
|
111 | auditor = pathauditor(root) | |
|
112 | if name != rootsep and name.startswith(rootsep): | |
|
113 | name = name[len(rootsep):] | |
|
114 | auditor(name) | |
|
115 | return util.pconvert(name) | |
|
116 | elif name == root: | |
|
117 | return '' | |
|
118 | else: | |
|
119 | # Determine whether `name' is in the hierarchy at or beneath `root', | |
|
120 | # by iterating name=dirname(name) until that causes no change (can't | |
|
121 | # check name == '/', because that doesn't work on windows). The list | |
|
122 | # `rel' holds the reversed list of components making up the relative | |
|
123 | # file name we want. | |
|
124 | rel = [] | |
|
125 | while True: | |
|
126 | try: | |
|
127 | s = util.samefile(name, root) | |
|
128 | except OSError: | |
|
129 | s = False | |
|
130 | if s: | |
|
131 | if not rel: | |
|
132 | # name was actually the same as root (maybe a symlink) | |
|
133 | return '' | |
|
134 | rel.reverse() | |
|
135 | name = os.path.join(*rel) | |
|
136 | auditor(name) | |
|
137 | return util.pconvert(name) | |
|
138 | dirname, basename = util.split(name) | |
|
139 | rel.append(basename) | |
|
140 | if dirname == name: | |
|
141 | break | |
|
142 | name = dirname | |
|
143 | ||
|
144 | raise util.Abort(_("%s not under root '%s'") % (myname, root)) |
@@ -84,7 +84,7 b" like CVS' $Log$, are not supported. A ke" | |||
|
84 | 84 | |
|
85 | 85 | from mercurial import commands, context, cmdutil, dispatch, filelog, extensions |
|
86 | 86 | from mercurial import localrepo, match, patch, templatefilters, templater, util |
|
87 | from mercurial import scmutil | |
|
87 | from mercurial import scmutil, pathutil | |
|
88 | 88 | from mercurial.hgweb import webcommands |
|
89 | 89 | from mercurial.i18n import _ |
|
90 | 90 | import os, re, shutil, tempfile |
@@ -673,7 +673,7 b' def reposetup(ui, repo):' | |||
|
673 | 673 | expansion. ''' |
|
674 | 674 | source = repo.dirstate.copied(dest) |
|
675 | 675 | if 'l' in wctx.flags(source): |
|
676 |
source = |
|
|
676 | source = pathutil.canonpath(repo.root, cwd, | |
|
677 | 677 | os.path.realpath(source)) |
|
678 | 678 | return kwt.match(source) |
|
679 | 679 |
@@ -12,7 +12,7 b' import os' | |||
|
12 | 12 | import copy |
|
13 | 13 | |
|
14 | 14 | from mercurial import hg, commands, util, cmdutil, scmutil, match as match_, \ |
|
15 | node, archival, error, merge, discovery | |
|
15 | node, archival, error, merge, discovery, pathutil | |
|
16 | 16 | from mercurial.i18n import _ |
|
17 | 17 | from mercurial.node import hex |
|
18 | 18 | from hgext import rebase |
@@ -469,7 +469,7 b' def overridecopy(orig, ui, repo, pats, o' | |||
|
469 | 469 | return orig(ui, repo, pats, opts, rename) |
|
470 | 470 | |
|
471 | 471 | def makestandin(relpath): |
|
472 |
path = |
|
|
472 | path = pathutil.canonpath(repo.root, repo.getcwd(), relpath) | |
|
473 | 473 | return os.path.join(repo.wjoin(lfutil.standin(path))) |
|
474 | 474 | |
|
475 | 475 | fullpats = scmutil.expandpats(pats) |
@@ -10,7 +10,7 b' from i18n import _' | |||
|
10 | 10 | import os, sys, errno, re, tempfile |
|
11 | 11 | import util, scmutil, templater, patch, error, templatekw, revlog, copies |
|
12 | 12 | import match as matchmod |
|
13 | import subrepo, context, repair, graphmod, revset, phases, obsolete | |
|
13 | import subrepo, context, repair, graphmod, revset, phases, obsolete, pathutil | |
|
14 | 14 | import changelog |
|
15 | 15 | import bookmarks |
|
16 | 16 | import lock as lockmod |
@@ -274,7 +274,7 b' def copy(ui, repo, pats, opts, rename=Fa' | |||
|
274 | 274 | # relsrc: ossep |
|
275 | 275 | # otarget: ossep |
|
276 | 276 | def copyfile(abssrc, relsrc, otarget, exact): |
|
277 |
abstarget = |
|
|
277 | abstarget = pathutil.canonpath(repo.root, cwd, otarget) | |
|
278 | 278 | if '/' in abstarget: |
|
279 | 279 | # We cannot normalize abstarget itself, this would prevent |
|
280 | 280 | # case only renames, like a => A. |
@@ -367,7 +367,7 b' def copy(ui, repo, pats, opts, rename=Fa' | |||
|
367 | 367 | # return: function that takes hgsep and returns ossep |
|
368 | 368 | def targetpathfn(pat, dest, srcs): |
|
369 | 369 | if os.path.isdir(pat): |
|
370 |
abspfx = |
|
|
370 | abspfx = pathutil.canonpath(repo.root, cwd, pat) | |
|
371 | 371 | abspfx = util.localpath(abspfx) |
|
372 | 372 | if destdirexists: |
|
373 | 373 | striplen = len(os.path.split(abspfx)[0]) |
@@ -393,7 +393,7 b' def copy(ui, repo, pats, opts, rename=Fa' | |||
|
393 | 393 | res = lambda p: os.path.join(dest, |
|
394 | 394 | os.path.basename(util.localpath(p))) |
|
395 | 395 | else: |
|
396 |
abspfx = |
|
|
396 | abspfx = pathutil.canonpath(repo.root, cwd, pat) | |
|
397 | 397 | if len(abspfx) < len(srcs[0][0]): |
|
398 | 398 | # A directory. Either the target path contains the last |
|
399 | 399 | # component of the source path or it does not. |
@@ -2065,7 +2065,7 b' def revert(ui, repo, ctx, parents, *pats' | |||
|
2065 | 2065 | fc = ctx[f] |
|
2066 | 2066 | repo.wwrite(f, fc.data(), fc.flags()) |
|
2067 | 2067 | |
|
2068 |
audit_path = |
|
|
2068 | audit_path = pathutil.pathauditor(repo.root) | |
|
2069 | 2069 | for f in remove[0]: |
|
2070 | 2070 | if repo.dirstate[f] == 'a': |
|
2071 | 2071 | repo.dirstate.drop(f) |
@@ -8,7 +8,7 b' import errno' | |||
|
8 | 8 | |
|
9 | 9 | from node import nullid |
|
10 | 10 | from i18n import _ |
|
11 | import scmutil, util, ignore, osutil, parsers, encoding | |
|
11 | import scmutil, util, ignore, osutil, parsers, encoding, pathutil | |
|
12 | 12 | import os, stat, errno, gc |
|
13 | 13 | |
|
14 | 14 | propertycache = util.propertycache |
@@ -736,7 +736,7 b' class dirstate(object):' | |||
|
736 | 736 | # unknown == True means we walked the full directory tree above. |
|
737 | 737 | # So if a file is not seen it was either a) not matching matchfn |
|
738 | 738 | # b) ignored, c) missing, or d) under a symlink directory. |
|
739 |
audit_path = |
|
|
739 | audit_path = pathutil.pathauditor(self._root) | |
|
740 | 740 | |
|
741 | 741 | for nf in iter(visit): |
|
742 | 742 | # Report ignored items in the dmap as long as they are not |
@@ -7,7 +7,7 b'' | |||
|
7 | 7 | # GNU General Public License version 2 or any later version. |
|
8 | 8 | |
|
9 | 9 | import os, copy |
|
10 |
from mercurial import match, patch, |
|
|
10 | from mercurial import match, patch, error, ui, util, pathutil | |
|
11 | 11 | from mercurial.i18n import _ |
|
12 | 12 | from mercurial.node import hex, nullid |
|
13 | 13 | from common import ErrorResponse |
@@ -196,7 +196,7 b' def showbookmark(repo, tmpl, t1, node=nu' | |||
|
196 | 196 | |
|
197 | 197 | def cleanpath(repo, path): |
|
198 | 198 | path = path.lstrip('/') |
|
199 |
return |
|
|
199 | return pathutil.canonpath(repo.root, '', path) | |
|
200 | 200 | |
|
201 | 201 | def changeidctx (repo, changeid): |
|
202 | 202 | try: |
@@ -15,7 +15,7 b' import merge as mergemod' | |||
|
15 | 15 | import tags as tagsmod |
|
16 | 16 | from lock import release |
|
17 | 17 | import weakref, errno, os, time, inspect |
|
18 | import branchmap | |
|
18 | import branchmap, pathutil | |
|
19 | 19 | propertycache = util.propertycache |
|
20 | 20 | filecache = scmutil.filecache |
|
21 | 21 | |
@@ -166,7 +166,7 b' class localrepository(object):' | |||
|
166 | 166 | self.root = self.wvfs.base |
|
167 | 167 | self.path = self.wvfs.join(".hg") |
|
168 | 168 | self.origroot = path |
|
169 |
self.auditor = |
|
|
169 | self.auditor = pathutil.pathauditor(self.root, self._checknested) | |
|
170 | 170 | self.vfs = scmutil.vfs(self.path) |
|
171 | 171 | self.opener = self.vfs |
|
172 | 172 | self.baseui = baseui |
@@ -6,7 +6,7 b'' | |||
|
6 | 6 | # GNU General Public License version 2 or any later version. |
|
7 | 7 | |
|
8 | 8 | import re |
|
9 |
import |
|
|
9 | import util, fileset, pathutil | |
|
10 | 10 | from i18n import _ |
|
11 | 11 | |
|
12 | 12 | def _rematcher(pat): |
@@ -317,7 +317,7 b' def _normalize(names, default, root, cwd' | |||
|
317 | 317 | pats = [] |
|
318 | 318 | for kind, name in [_patsplit(p, default) for p in names]: |
|
319 | 319 | if kind in ('glob', 'relpath'): |
|
320 |
name = |
|
|
320 | name = pathutil.canonpath(root, cwd, name, auditor) | |
|
321 | 321 | elif kind in ('relglob', 'path'): |
|
322 | 322 | name = util.normpath(name) |
|
323 | 323 | elif kind in ('listfile', 'listfile0'): |
@@ -8,8 +8,9 b'' | |||
|
8 | 8 | from i18n import _ |
|
9 | 9 | from mercurial.node import nullrev |
|
10 | 10 | import util, error, osutil, revset, similar, encoding, phases, parsers |
|
11 | import pathutil | |
|
11 | 12 | import match as matchmod |
|
12 |
import os, errno, re, |
|
|
13 | import os, errno, re, glob | |
|
13 | 14 | |
|
14 | 15 | if os.name == 'nt': |
|
15 | 16 | import scmwindows as scmplatform |
@@ -108,100 +109,6 b' class casecollisionauditor(object):' | |||
|
108 | 109 | self._loweredfiles.add(fl) |
|
109 | 110 | self._newfiles.add(f) |
|
110 | 111 | |
|
111 | class pathauditor(object): | |
|
112 | '''ensure that a filesystem path contains no banned components. | |
|
113 | the following properties of a path are checked: | |
|
114 | ||
|
115 | - ends with a directory separator | |
|
116 | - under top-level .hg | |
|
117 | - starts at the root of a windows drive | |
|
118 | - contains ".." | |
|
119 | - traverses a symlink (e.g. a/symlink_here/b) | |
|
120 | - inside a nested repository (a callback can be used to approve | |
|
121 | some nested repositories, e.g., subrepositories) | |
|
122 | ''' | |
|
123 | ||
|
124 | def __init__(self, root, callback=None): | |
|
125 | self.audited = set() | |
|
126 | self.auditeddir = set() | |
|
127 | self.root = root | |
|
128 | self.callback = callback | |
|
129 | if os.path.lexists(root) and not util.checkcase(root): | |
|
130 | self.normcase = util.normcase | |
|
131 | else: | |
|
132 | self.normcase = lambda x: x | |
|
133 | ||
|
134 | def __call__(self, path): | |
|
135 | '''Check the relative path. | |
|
136 | path may contain a pattern (e.g. foodir/**.txt)''' | |
|
137 | ||
|
138 | path = util.localpath(path) | |
|
139 | normpath = self.normcase(path) | |
|
140 | if normpath in self.audited: | |
|
141 | return | |
|
142 | # AIX ignores "/" at end of path, others raise EISDIR. | |
|
143 | if util.endswithsep(path): | |
|
144 | raise util.Abort(_("path ends in directory separator: %s") % path) | |
|
145 | parts = util.splitpath(path) | |
|
146 | if (os.path.splitdrive(path)[0] | |
|
147 | or parts[0].lower() in ('.hg', '.hg.', '') | |
|
148 | or os.pardir in parts): | |
|
149 | raise util.Abort(_("path contains illegal component: %s") % path) | |
|
150 | if '.hg' in path.lower(): | |
|
151 | lparts = [p.lower() for p in parts] | |
|
152 | for p in '.hg', '.hg.': | |
|
153 | if p in lparts[1:]: | |
|
154 | pos = lparts.index(p) | |
|
155 | base = os.path.join(*parts[:pos]) | |
|
156 | raise util.Abort(_("path '%s' is inside nested repo %r") | |
|
157 | % (path, base)) | |
|
158 | ||
|
159 | normparts = util.splitpath(normpath) | |
|
160 | assert len(parts) == len(normparts) | |
|
161 | ||
|
162 | parts.pop() | |
|
163 | normparts.pop() | |
|
164 | prefixes = [] | |
|
165 | while parts: | |
|
166 | prefix = os.sep.join(parts) | |
|
167 | normprefix = os.sep.join(normparts) | |
|
168 | if normprefix in self.auditeddir: | |
|
169 | break | |
|
170 | curpath = os.path.join(self.root, prefix) | |
|
171 | try: | |
|
172 | st = os.lstat(curpath) | |
|
173 | except OSError, err: | |
|
174 | # EINVAL can be raised as invalid path syntax under win32. | |
|
175 | # They must be ignored for patterns can be checked too. | |
|
176 | if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL): | |
|
177 | raise | |
|
178 | else: | |
|
179 | if stat.S_ISLNK(st.st_mode): | |
|
180 | raise util.Abort( | |
|
181 | _('path %r traverses symbolic link %r') | |
|
182 | % (path, prefix)) | |
|
183 | elif (stat.S_ISDIR(st.st_mode) and | |
|
184 | os.path.isdir(os.path.join(curpath, '.hg'))): | |
|
185 | if not self.callback or not self.callback(curpath): | |
|
186 | raise util.Abort(_("path '%s' is inside nested " | |
|
187 | "repo %r") | |
|
188 | % (path, prefix)) | |
|
189 | prefixes.append(normprefix) | |
|
190 | parts.pop() | |
|
191 | normparts.pop() | |
|
192 | ||
|
193 | self.audited.add(normpath) | |
|
194 | # only add prefixes to the cache after checking everything: we don't | |
|
195 | # want to add "foo/bar/baz" before checking if there's a "foo/.hg" | |
|
196 | self.auditeddir.update(prefixes) | |
|
197 | ||
|
198 | def check(self, path): | |
|
199 | try: | |
|
200 | self(path) | |
|
201 | return True | |
|
202 | except (OSError, util.Abort): | |
|
203 | return False | |
|
204 | ||
|
205 | 112 | class abstractvfs(object): |
|
206 | 113 | """Abstract base class; cannot be instantiated""" |
|
207 | 114 | |
@@ -310,7 +217,7 b' class vfs(abstractvfs):' | |||
|
310 | 217 | def _setmustaudit(self, onoff): |
|
311 | 218 | self._audit = onoff |
|
312 | 219 | if onoff: |
|
313 | self.audit = pathauditor(self.base) | |
|
220 | self.audit = pathutil.pathauditor(self.base) | |
|
314 | 221 | else: |
|
315 | 222 | self.audit = util.always |
|
316 | 223 | |
@@ -445,52 +352,6 b' class readonlyvfs(abstractvfs, auditvfs)' | |||
|
445 | 352 | return self.vfs(path, mode, *args, **kw) |
|
446 | 353 | |
|
447 | 354 | |
|
448 | def canonpath(root, cwd, myname, auditor=None): | |
|
449 | '''return the canonical path of myname, given cwd and root''' | |
|
450 | if util.endswithsep(root): | |
|
451 | rootsep = root | |
|
452 | else: | |
|
453 | rootsep = root + os.sep | |
|
454 | name = myname | |
|
455 | if not os.path.isabs(name): | |
|
456 | name = os.path.join(root, cwd, name) | |
|
457 | name = os.path.normpath(name) | |
|
458 | if auditor is None: | |
|
459 | auditor = pathauditor(root) | |
|
460 | if name != rootsep and name.startswith(rootsep): | |
|
461 | name = name[len(rootsep):] | |
|
462 | auditor(name) | |
|
463 | return util.pconvert(name) | |
|
464 | elif name == root: | |
|
465 | return '' | |
|
466 | else: | |
|
467 | # Determine whether `name' is in the hierarchy at or beneath `root', | |
|
468 | # by iterating name=dirname(name) until that causes no change (can't | |
|
469 | # check name == '/', because that doesn't work on windows). The list | |
|
470 | # `rel' holds the reversed list of components making up the relative | |
|
471 | # file name we want. | |
|
472 | rel = [] | |
|
473 | while True: | |
|
474 | try: | |
|
475 | s = util.samefile(name, root) | |
|
476 | except OSError: | |
|
477 | s = False | |
|
478 | if s: | |
|
479 | if not rel: | |
|
480 | # name was actually the same as root (maybe a symlink) | |
|
481 | return '' | |
|
482 | rel.reverse() | |
|
483 | name = os.path.join(*rel) | |
|
484 | auditor(name) | |
|
485 | return util.pconvert(name) | |
|
486 | dirname, basename = util.split(name) | |
|
487 | rel.append(basename) | |
|
488 | if dirname == name: | |
|
489 | break | |
|
490 | name = dirname | |
|
491 | ||
|
492 | raise util.Abort(_("%s not under root '%s'") % (myname, root)) | |
|
493 | ||
|
494 | 355 | def walkrepos(path, followsym=False, seen_dirs=None, recurse=False): |
|
495 | 356 | '''yield every hg repository under path, always recursively. |
|
496 | 357 | The recurse flag will only control recursion into repo working dirs''' |
@@ -768,7 +629,7 b' def _interestingfiles(repo, matcher):' | |||
|
768 | 629 | This is different from dirstate.status because it doesn't care about |
|
769 | 630 | whether files are modified or clean.''' |
|
770 | 631 | added, unknown, deleted, removed = [], [], [], [] |
|
771 | audit_path = pathauditor(repo.root) | |
|
632 | audit_path = pathutil.pathauditor(repo.root) | |
|
772 | 633 | |
|
773 | 634 | ctx = repo[None] |
|
774 | 635 | dirstate = repo.dirstate |
@@ -9,7 +9,8 b' import errno, os, re, shutil, posixpath,' | |||
|
9 | 9 | import xml.dom.minidom |
|
10 | 10 | import stat, subprocess, tarfile |
|
11 | 11 | from i18n import _ |
|
12 |
import config |
|
|
12 | import config, util, node, error, cmdutil, bookmarks, match as matchmod | |
|
13 | import pathutil | |
|
13 | 14 | hg = None |
|
14 | 15 | propertycache = util.propertycache |
|
15 | 16 | |
@@ -332,7 +333,7 b' def subrepo(ctx, path):' | |||
|
332 | 333 | import hg as h |
|
333 | 334 | hg = h |
|
334 | 335 | |
|
335 |
|
|
|
336 | pathutil.pathauditor(ctx._repo.root)(path) | |
|
336 | 337 | state = ctx.substate[path] |
|
337 | 338 | if state[2] not in types: |
|
338 | 339 | raise util.Abort(_('unknown subrepo type %s') % state[2]) |
General Comments 0
You need to be logged in to leave comments.
Login now