##// END OF EJS Templates
hgwebdir: fix incorrect index generation for invalid paths (issue2023)...
Wagner Bruna -
r13066:86888ae9 stable
parent child Browse files
Show More
@@ -1,356 +1,357 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import os, re, time, urlparse
10 10 from mercurial.i18n import _
11 11 from mercurial import ui, hg, util, templater
12 12 from mercurial import error, encoding
13 13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
14 14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 15 from hgweb_mod import hgweb
16 16 from request import wsgirequest
17 17 import webutil
18 18
19 19 def cleannames(items):
20 20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21 21
22 22 def findrepos(paths):
23 23 repos = []
24 24 for prefix, root in cleannames(paths):
25 25 roothead, roottail = os.path.split(root)
26 26 # "foo = /bar/*" makes every subrepo of /bar/ to be
27 27 # mounted as foo/subrepo
28 28 # and "foo = /bar/**" also recurses into the subdirectories,
29 29 # remember to use it without working dir.
30 30 try:
31 31 recurse = {'*': False, '**': True}[roottail]
32 32 except KeyError:
33 33 repos.append((prefix, root))
34 34 continue
35 35 roothead = os.path.normpath(os.path.abspath(roothead))
36 36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
37 37 path = os.path.normpath(path)
38 38 name = util.pconvert(path[len(roothead):]).strip('/')
39 39 if prefix:
40 40 name = prefix + '/' + name
41 41 repos.append((name, path))
42 42 return repos
43 43
44 44 class hgwebdir(object):
45 45 refreshinterval = 20
46 46
47 47 def __init__(self, conf, baseui=None):
48 48 self.conf = conf
49 49 self.baseui = baseui
50 50 self.lastrefresh = 0
51 51 self.motd = None
52 52 self.refresh()
53 53
54 54 def refresh(self):
55 55 if self.lastrefresh + self.refreshinterval > time.time():
56 56 return
57 57
58 58 if self.baseui:
59 59 u = self.baseui.copy()
60 60 else:
61 61 u = ui.ui()
62 62 u.setconfig('ui', 'report_untrusted', 'off')
63 63 u.setconfig('ui', 'interactive', 'off')
64 64
65 65 if not isinstance(self.conf, (dict, list, tuple)):
66 66 map = {'paths': 'hgweb-paths'}
67 67 u.readconfig(self.conf, remap=map, trust=True)
68 68 paths = u.configitems('hgweb-paths')
69 69 elif isinstance(self.conf, (list, tuple)):
70 70 paths = self.conf
71 71 elif isinstance(self.conf, dict):
72 72 paths = self.conf.items()
73 73
74 74 repos = findrepos(paths)
75 75 for prefix, root in u.configitems('collections'):
76 76 prefix = util.pconvert(prefix)
77 77 for path in util.walkrepos(root, followsym=True):
78 78 repo = os.path.normpath(path)
79 79 name = util.pconvert(repo)
80 80 if name.startswith(prefix):
81 81 name = name[len(prefix):]
82 82 repos.append((name.lstrip('/'), repo))
83 83
84 84 self.repos = repos
85 85 self.ui = u
86 86 encoding.encoding = self.ui.config('web', 'encoding',
87 87 encoding.encoding)
88 88 self.style = self.ui.config('web', 'style', 'paper')
89 89 self.templatepath = self.ui.config('web', 'templates', None)
90 90 self.stripecount = self.ui.config('web', 'stripes', 1)
91 91 if self.stripecount:
92 92 self.stripecount = int(self.stripecount)
93 93 self._baseurl = self.ui.config('web', 'baseurl')
94 94 self.lastrefresh = time.time()
95 95
96 96 def run(self):
97 97 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
98 98 raise RuntimeError("This function is only intended to be "
99 99 "called while running as a CGI script.")
100 100 import mercurial.hgweb.wsgicgi as wsgicgi
101 101 wsgicgi.launch(self)
102 102
103 103 def __call__(self, env, respond):
104 104 req = wsgirequest(env, respond)
105 105 return self.run_wsgi(req)
106 106
107 107 def read_allowed(self, ui, req):
108 108 """Check allow_read and deny_read config options of a repo's ui object
109 109 to determine user permissions. By default, with neither option set (or
110 110 both empty), allow all users to read the repo. There are two ways a
111 111 user can be denied read access: (1) deny_read is not empty, and the
112 112 user is unauthenticated or deny_read contains user (or *), and (2)
113 113 allow_read is not empty and the user is not in allow_read. Return True
114 114 if user is allowed to read the repo, else return False."""
115 115
116 116 user = req.env.get('REMOTE_USER')
117 117
118 118 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
119 119 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
120 120 return False
121 121
122 122 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
123 123 # by default, allow reading if no allow_read option has been set
124 124 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
125 125 return True
126 126
127 127 return False
128 128
129 129 def run_wsgi(self, req):
130 130 try:
131 131 try:
132 132 self.refresh()
133 133
134 134 virtual = req.env.get("PATH_INFO", "").strip('/')
135 135 tmpl = self.templater(req)
136 136 ctype = tmpl('mimetype', encoding=encoding.encoding)
137 137 ctype = templater.stringify(ctype)
138 138
139 139 # a static file
140 140 if virtual.startswith('static/') or 'static' in req.form:
141 141 if virtual.startswith('static/'):
142 142 fname = virtual[7:]
143 143 else:
144 144 fname = req.form['static'][0]
145 145 static = templater.templatepath('static')
146 146 return (staticfile(static, fname, req),)
147 147
148 148 # top-level index
149 149 elif not virtual:
150 150 req.respond(HTTP_OK, ctype)
151 151 return self.makeindex(req, tmpl)
152 152
153 153 # nested indexes and hgwebs
154 154
155 155 repos = dict(self.repos)
156 while virtual:
157 real = repos.get(virtual)
156 virtualrepo = virtual
157 while virtualrepo:
158 real = repos.get(virtualrepo)
158 159 if real:
159 req.env['REPO_NAME'] = virtual
160 req.env['REPO_NAME'] = virtualrepo
160 161 try:
161 162 repo = hg.repository(self.ui, real)
162 163 return hgweb(repo).run_wsgi(req)
163 164 except IOError, inst:
164 165 msg = inst.strerror
165 166 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
166 167 except error.RepoError, inst:
167 168 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
168 169
169 # browse subdirectories
170 subdir = virtual + '/'
171 if [r for r in repos if r.startswith(subdir)]:
172 req.respond(HTTP_OK, ctype)
173 return self.makeindex(req, tmpl, subdir)
174
175 up = virtual.rfind('/')
170 up = virtualrepo.rfind('/')
176 171 if up < 0:
177 172 break
178 virtual = virtual[:up]
173 virtualrepo = virtualrepo[:up]
174
175 # browse subdirectories
176 subdir = virtual + '/'
177 if [r for r in repos if r.startswith(subdir)]:
178 req.respond(HTTP_OK, ctype)
179 return self.makeindex(req, tmpl, subdir)
179 180
180 181 # prefixes not found
181 182 req.respond(HTTP_NOT_FOUND, ctype)
182 183 return tmpl("notfound", repo=virtual)
183 184
184 185 except ErrorResponse, err:
185 186 req.respond(err, ctype)
186 187 return tmpl('error', error=err.message or '')
187 188 finally:
188 189 tmpl = None
189 190
190 191 def makeindex(self, req, tmpl, subdir=""):
191 192
192 193 def archivelist(ui, nodeid, url):
193 194 allowed = ui.configlist("web", "allow_archive", untrusted=True)
194 195 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
195 196 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
196 197 untrusted=True):
197 198 yield {"type" : i[0], "extension": i[1],
198 199 "node": nodeid, "url": url}
199 200
200 201 def rawentries(subdir="", **map):
201 202
202 203 descend = self.ui.configbool('web', 'descend', True)
203 204 for name, path in self.repos:
204 205
205 206 if not name.startswith(subdir):
206 207 continue
207 208 name = name[len(subdir):]
208 209 if not descend and '/' in name:
209 210 continue
210 211
211 212 u = self.ui.copy()
212 213 try:
213 214 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
214 215 except Exception, e:
215 216 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
216 217 continue
217 218 def get(section, name, default=None):
218 219 return u.config(section, name, default, untrusted=True)
219 220
220 221 if u.configbool("web", "hidden", untrusted=True):
221 222 continue
222 223
223 224 if not self.read_allowed(u, req):
224 225 continue
225 226
226 227 parts = [name]
227 228 if 'PATH_INFO' in req.env:
228 229 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
229 230 if req.env['SCRIPT_NAME']:
230 231 parts.insert(0, req.env['SCRIPT_NAME'])
231 232 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
232 233
233 234 # update time with local timezone
234 235 try:
235 236 r = hg.repository(self.ui, path)
236 237 except error.RepoError:
237 238 u.warn(_('error accessing repository at %s\n') % path)
238 239 continue
239 240 try:
240 241 d = (get_mtime(r.spath), util.makedate()[1])
241 242 except OSError:
242 243 continue
243 244
244 245 contact = get_contact(get)
245 246 description = get("web", "description", "")
246 247 name = get("web", "name", name)
247 248 row = dict(contact=contact or "unknown",
248 249 contact_sort=contact.upper() or "unknown",
249 250 name=name,
250 251 name_sort=name,
251 252 url=url,
252 253 description=description or "unknown",
253 254 description_sort=description.upper() or "unknown",
254 255 lastchange=d,
255 256 lastchange_sort=d[1]-d[0],
256 257 archives=archivelist(u, "tip", url))
257 258 yield row
258 259
259 260 sortdefault = None, False
260 261 def entries(sortcolumn="", descending=False, subdir="", **map):
261 262 rows = rawentries(subdir=subdir, **map)
262 263
263 264 if sortcolumn and sortdefault != (sortcolumn, descending):
264 265 sortkey = '%s_sort' % sortcolumn
265 266 rows = sorted(rows, key=lambda x: x[sortkey],
266 267 reverse=descending)
267 268 for row, parity in zip(rows, paritygen(self.stripecount)):
268 269 row['parity'] = parity
269 270 yield row
270 271
271 272 self.refresh()
272 273 sortable = ["name", "description", "contact", "lastchange"]
273 274 sortcolumn, descending = sortdefault
274 275 if 'sort' in req.form:
275 276 sortcolumn = req.form['sort'][0]
276 277 descending = sortcolumn.startswith('-')
277 278 if descending:
278 279 sortcolumn = sortcolumn[1:]
279 280 if sortcolumn not in sortable:
280 281 sortcolumn = ""
281 282
282 283 sort = [("sort_%s" % column,
283 284 "%s%s" % ((not descending and column == sortcolumn)
284 285 and "-" or "", column))
285 286 for column in sortable]
286 287
287 288 self.refresh()
288 289 self.updatereqenv(req.env)
289 290
290 291 return tmpl("index", entries=entries, subdir=subdir,
291 292 sortcolumn=sortcolumn, descending=descending,
292 293 **dict(sort))
293 294
294 295 def templater(self, req):
295 296
296 297 def header(**map):
297 298 yield tmpl('header', encoding=encoding.encoding, **map)
298 299
299 300 def footer(**map):
300 301 yield tmpl("footer", **map)
301 302
302 303 def motd(**map):
303 304 if self.motd is not None:
304 305 yield self.motd
305 306 else:
306 307 yield config('web', 'motd', '')
307 308
308 309 def config(section, name, default=None, untrusted=True):
309 310 return self.ui.config(section, name, default, untrusted)
310 311
311 312 self.updatereqenv(req.env)
312 313
313 314 url = req.env.get('SCRIPT_NAME', '')
314 315 if not url.endswith('/'):
315 316 url += '/'
316 317
317 318 vars = {}
318 319 styles = (
319 320 req.form.get('style', [None])[0],
320 321 config('web', 'style'),
321 322 'paper'
322 323 )
323 324 style, mapfile = templater.stylemap(styles, self.templatepath)
324 325 if style == styles[0]:
325 326 vars['style'] = style
326 327
327 328 start = url[-1] == '?' and '&' or '?'
328 329 sessionvars = webutil.sessionvars(vars, start)
329 330 staticurl = config('web', 'staticurl') or url + 'static/'
330 331 if not staticurl.endswith('/'):
331 332 staticurl += '/'
332 333
333 334 tmpl = templater.templater(mapfile,
334 335 defaults={"header": header,
335 336 "footer": footer,
336 337 "motd": motd,
337 338 "url": url,
338 339 "staticurl": staticurl,
339 340 "sessionvars": sessionvars})
340 341 return tmpl
341 342
342 343 def updatereqenv(self, env):
343 344 def splitnetloc(netloc):
344 345 if ':' in netloc:
345 346 return netloc.split(':', 1)
346 347 else:
347 348 return (netloc, None)
348 349
349 350 if self._baseurl is not None:
350 351 urlcomp = urlparse.urlparse(self._baseurl)
351 352 host, port = splitnetloc(urlcomp[1])
352 353 path = urlcomp[2]
353 354 env['SERVER_NAME'] = host
354 355 if port:
355 356 env['SERVER_PORT'] = port
356 357 env['SCRIPT_NAME'] = path
@@ -1,648 +1,648 b''
1 1 Tests some basic hgwebdir functionality. Tests setting up paths and
2 2 collection, different forms of 404s and the subdirectory support.
3 3
4 4 $ mkdir webdir
5 5 $ cd webdir
6 6 $ hg init a
7 7 $ echo a > a/a
8 8 $ hg --cwd a ci -Ama -d'1 0'
9 9 adding a
10 10
11 11 create a mercurial queue repository
12 12
13 13 $ hg --cwd a qinit --config extensions.hgext.mq= -c
14 14 $ hg init b
15 15 $ echo b > b/b
16 16 $ hg --cwd b ci -Amb -d'2 0'
17 17 adding b
18 18
19 19 create a nested repository
20 20
21 21 $ cd b
22 22 $ hg init d
23 23 $ echo d > d/d
24 24 $ hg --cwd d ci -Amd -d'3 0'
25 25 adding d
26 26 $ cd ..
27 27 $ hg init c
28 28 $ echo c > c/c
29 29 $ hg --cwd c ci -Amc -d'3 0'
30 30 adding c
31 31
32 32 create repository without .hg/store
33 33
34 34 $ hg init nostore
35 35 $ rm -R nostore/.hg/store
36 36 $ root=`pwd`
37 37 $ cd ..
38 38 $ cat > paths.conf <<EOF
39 39 > [paths]
40 40 > a=$root/a
41 41 > b=$root/b
42 42 > EOF
43 43 $ hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf paths.conf \
44 44 > -A access-paths.log -E error-paths-1.log
45 45 $ cat hg.pid >> $DAEMON_PIDS
46 46
47 47 should give a 404 - file does not exist
48 48
49 49 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/bork?style=raw'
50 50 404 Not Found
51 51
52 52
53 53 error: bork@8580ff50825a: not found in manifest
54 54 [1]
55 55
56 56 should succeed
57 57
58 58 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/?style=raw'
59 59 200 Script output follows
60 60
61 61
62 62 /a/
63 63 /b/
64 64
65 65 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/a?style=raw'
66 66 200 Script output follows
67 67
68 68 a
69 69 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/b/file/tip/b?style=raw'
70 70 200 Script output follows
71 71
72 72 b
73 73
74 74 should give a 404 - repo is not published
75 75
76 76 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/c/file/tip/c?style=raw'
77 77 404 Not Found
78 78
79 79
80 error: repository c not found
80 error: repository c/file/tip/c not found
81 81 [1]
82 82
83 83 atom-log without basedir
84 84
85 85 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/atom-log' | grep '<link'
86 86 <link rel="self" href="http://*:$HGPORT/a/atom-log"/> (glob)
87 87 <link rel="alternate" href="http://*:$HGPORT/a/"/> (glob)
88 88 <link href="http://*:$HGPORT/a/rev/8580ff50825a"/> (glob)
89 89
90 90 rss-log without basedir
91 91
92 92 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/rss-log' | grep '<guid'
93 93 <guid isPermaLink="true">http://*:$HGPORT/a/rev/8580ff50825a</guid> (glob)
94 94 $ cat > paths.conf <<EOF
95 95 > [paths]
96 96 > t/a/=$root/a
97 97 > b=$root/b
98 98 > coll=$root/*
99 99 > rcoll=$root/**
100 100 > star=*
101 101 > starstar=**
102 102 > EOF
103 103 $ hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf paths.conf \
104 104 > -A access-paths.log -E error-paths-2.log
105 105 $ cat hg.pid >> $DAEMON_PIDS
106 106
107 107 should succeed, slashy names
108 108
109 109 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=raw'
110 110 200 Script output follows
111 111
112 112
113 113 /t/a/
114 114 /b/
115 115 /coll/a/
116 116 /coll/a/.hg/patches/
117 117 /coll/b/
118 118 /coll/c/
119 119 /rcoll/a/
120 120 /rcoll/a/.hg/patches/
121 121 /rcoll/b/
122 122 /rcoll/b/d/
123 123 /rcoll/c/
124 124 /star/webdir/a/
125 125 /star/webdir/a/.hg/patches/
126 126 /star/webdir/b/
127 127 /star/webdir/c/
128 128 /starstar/webdir/a/
129 129 /starstar/webdir/a/.hg/patches/
130 130 /starstar/webdir/b/
131 131 /starstar/webdir/b/d/
132 132 /starstar/webdir/c/
133 133
134 134 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=paper'
135 135 200 Script output follows
136 136
137 137 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
138 138 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
139 139 <head>
140 140 <link rel="icon" href="/static/hgicon.png" type="image/png" />
141 141 <meta name="robots" content="index, nofollow" />
142 142 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
143 143
144 144 <title>Mercurial repositories index</title>
145 145 </head>
146 146 <body>
147 147
148 148 <div class="container">
149 149 <div class="menu">
150 150 <a href="http://mercurial.selenic.com/">
151 151 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
152 152 </div>
153 153 <div class="main">
154 154 <h2>Mercurial Repositories</h2>
155 155
156 156 <table class="bigtable">
157 157 <tr>
158 158 <th><a href="?sort=name">Name</a></th>
159 159 <th><a href="?sort=description">Description</a></th>
160 160 <th><a href="?sort=contact">Contact</a></th>
161 161 <th><a href="?sort=lastchange">Last modified</a></th>
162 162 <th>&nbsp;</th>
163 163 </tr>
164 164
165 165 <tr class="parity0">
166 166 <td><a href="/t/a/?style=paper">t/a</a></td>
167 167 <td>unknown</td>
168 168 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
169 169 <td class="age">* ago</td> (glob)
170 170 <td class="indexlinks"></td>
171 171 </tr>
172 172
173 173 <tr class="parity1">
174 174 <td><a href="/b/?style=paper">b</a></td>
175 175 <td>unknown</td>
176 176 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
177 177 <td class="age">* ago</td> (glob)
178 178 <td class="indexlinks"></td>
179 179 </tr>
180 180
181 181 <tr class="parity0">
182 182 <td><a href="/coll/a/?style=paper">coll/a</a></td>
183 183 <td>unknown</td>
184 184 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
185 185 <td class="age">* ago</td> (glob)
186 186 <td class="indexlinks"></td>
187 187 </tr>
188 188
189 189 <tr class="parity1">
190 190 <td><a href="/coll/a/.hg/patches/?style=paper">coll/a/.hg/patches</a></td>
191 191 <td>unknown</td>
192 192 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
193 193 <td class="age">* ago</td> (glob)
194 194 <td class="indexlinks"></td>
195 195 </tr>
196 196
197 197 <tr class="parity0">
198 198 <td><a href="/coll/b/?style=paper">coll/b</a></td>
199 199 <td>unknown</td>
200 200 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
201 201 <td class="age">* ago</td> (glob)
202 202 <td class="indexlinks"></td>
203 203 </tr>
204 204
205 205 <tr class="parity1">
206 206 <td><a href="/coll/c/?style=paper">coll/c</a></td>
207 207 <td>unknown</td>
208 208 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
209 209 <td class="age">* ago</td> (glob)
210 210 <td class="indexlinks"></td>
211 211 </tr>
212 212
213 213 <tr class="parity0">
214 214 <td><a href="/rcoll/a/?style=paper">rcoll/a</a></td>
215 215 <td>unknown</td>
216 216 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
217 217 <td class="age">* ago</td> (glob)
218 218 <td class="indexlinks"></td>
219 219 </tr>
220 220
221 221 <tr class="parity1">
222 222 <td><a href="/rcoll/a/.hg/patches/?style=paper">rcoll/a/.hg/patches</a></td>
223 223 <td>unknown</td>
224 224 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
225 225 <td class="age">* ago</td> (glob)
226 226 <td class="indexlinks"></td>
227 227 </tr>
228 228
229 229 <tr class="parity0">
230 230 <td><a href="/rcoll/b/?style=paper">rcoll/b</a></td>
231 231 <td>unknown</td>
232 232 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
233 233 <td class="age">* ago</td> (glob)
234 234 <td class="indexlinks"></td>
235 235 </tr>
236 236
237 237 <tr class="parity1">
238 238 <td><a href="/rcoll/b/d/?style=paper">rcoll/b/d</a></td>
239 239 <td>unknown</td>
240 240 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
241 241 <td class="age">* ago</td> (glob)
242 242 <td class="indexlinks"></td>
243 243 </tr>
244 244
245 245 <tr class="parity0">
246 246 <td><a href="/rcoll/c/?style=paper">rcoll/c</a></td>
247 247 <td>unknown</td>
248 248 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
249 249 <td class="age">* ago</td> (glob)
250 250 <td class="indexlinks"></td>
251 251 </tr>
252 252
253 253 <tr class="parity1">
254 254 <td><a href="/star/webdir/a/?style=paper">star/webdir/a</a></td>
255 255 <td>unknown</td>
256 256 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
257 257 <td class="age">* ago</td> (glob)
258 258 <td class="indexlinks"></td>
259 259 </tr>
260 260
261 261 <tr class="parity0">
262 262 <td><a href="/star/webdir/a/.hg/patches/?style=paper">star/webdir/a/.hg/patches</a></td>
263 263 <td>unknown</td>
264 264 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
265 265 <td class="age">* ago</td> (glob)
266 266 <td class="indexlinks"></td>
267 267 </tr>
268 268
269 269 <tr class="parity1">
270 270 <td><a href="/star/webdir/b/?style=paper">star/webdir/b</a></td>
271 271 <td>unknown</td>
272 272 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
273 273 <td class="age">* ago</td> (glob)
274 274 <td class="indexlinks"></td>
275 275 </tr>
276 276
277 277 <tr class="parity0">
278 278 <td><a href="/star/webdir/c/?style=paper">star/webdir/c</a></td>
279 279 <td>unknown</td>
280 280 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
281 281 <td class="age">* ago</td> (glob)
282 282 <td class="indexlinks"></td>
283 283 </tr>
284 284
285 285 <tr class="parity1">
286 286 <td><a href="/starstar/webdir/a/?style=paper">starstar/webdir/a</a></td>
287 287 <td>unknown</td>
288 288 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
289 289 <td class="age">* ago</td> (glob)
290 290 <td class="indexlinks"></td>
291 291 </tr>
292 292
293 293 <tr class="parity0">
294 294 <td><a href="/starstar/webdir/a/.hg/patches/?style=paper">starstar/webdir/a/.hg/patches</a></td>
295 295 <td>unknown</td>
296 296 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
297 297 <td class="age">* ago</td> (glob)
298 298 <td class="indexlinks"></td>
299 299 </tr>
300 300
301 301 <tr class="parity1">
302 302 <td><a href="/starstar/webdir/b/?style=paper">starstar/webdir/b</a></td>
303 303 <td>unknown</td>
304 304 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
305 305 <td class="age">* ago</td> (glob)
306 306 <td class="indexlinks"></td>
307 307 </tr>
308 308
309 309 <tr class="parity0">
310 310 <td><a href="/starstar/webdir/b/d/?style=paper">starstar/webdir/b/d</a></td>
311 311 <td>unknown</td>
312 312 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
313 313 <td class="age">* ago</td> (glob)
314 314 <td class="indexlinks"></td>
315 315 </tr>
316 316
317 317 <tr class="parity1">
318 318 <td><a href="/starstar/webdir/c/?style=paper">starstar/webdir/c</a></td>
319 319 <td>unknown</td>
320 320 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
321 321 <td class="age">* ago</td> (glob)
322 322 <td class="indexlinks"></td>
323 323 </tr>
324 324
325 325 </table>
326 326 </div>
327 327 </div>
328 328
329 329
330 330 </body>
331 331 </html>
332 332
333 333 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t?style=raw'
334 334 200 Script output follows
335 335
336 336
337 337 /t/a/
338 338
339 339 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/?style=raw'
340 340 200 Script output follows
341 341
342 342
343 343 /t/a/
344 344
345 345 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/?style=paper'
346 346 200 Script output follows
347 347
348 348 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
349 349 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
350 350 <head>
351 351 <link rel="icon" href="/static/hgicon.png" type="image/png" />
352 352 <meta name="robots" content="index, nofollow" />
353 353 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
354 354
355 355 <title>Mercurial repositories index</title>
356 356 </head>
357 357 <body>
358 358
359 359 <div class="container">
360 360 <div class="menu">
361 361 <a href="http://mercurial.selenic.com/">
362 362 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
363 363 </div>
364 364 <div class="main">
365 365 <h2>Mercurial Repositories</h2>
366 366
367 367 <table class="bigtable">
368 368 <tr>
369 369 <th><a href="?sort=name">Name</a></th>
370 370 <th><a href="?sort=description">Description</a></th>
371 371 <th><a href="?sort=contact">Contact</a></th>
372 372 <th><a href="?sort=lastchange">Last modified</a></th>
373 373 <th>&nbsp;</th>
374 374 </tr>
375 375
376 376 <tr class="parity0">
377 377 <td><a href="/t/a/?style=paper">a</a></td>
378 378 <td>unknown</td>
379 379 <td>&#70;&#111;&#111;&#32;&#66;&#97;&#114;&#32;&#60;&#102;&#111;&#111;&#46;&#98;&#97;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;&#62;</td>
380 380 <td class="age">* ago</td> (glob)
381 381 <td class="indexlinks"></td>
382 382 </tr>
383 383
384 384 </table>
385 385 </div>
386 386 </div>
387 387
388 388
389 389 </body>
390 390 </html>
391 391
392 392 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a?style=atom'
393 393 200 Script output follows
394 394
395 395 <?xml version="1.0" encoding="ascii"?>
396 396 <feed xmlns="http://www.w3.org/2005/Atom">
397 397 <!-- Changelog -->
398 398 <id>http://*:$HGPORT1/t/a/</id> (glob)
399 399 <link rel="self" href="http://*:$HGPORT1/t/a/atom-log"/> (glob)
400 400 <link rel="alternate" href="http://*:$HGPORT1/t/a/"/> (glob)
401 401 <title>t/a Changelog</title>
402 402 <updated>1970-01-01T00:00:01+00:00</updated>
403 403
404 404 <entry>
405 405 <title>a</title>
406 406 <id>http://*:$HGPORT1/t/a/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id> (glob)
407 407 <link href="http://*:$HGPORT1/t/a/rev/8580ff50825a"/> (glob)
408 408 <author>
409 409 <name>test</name>
410 410 <email>&#116;&#101;&#115;&#116;</email>
411 411 </author>
412 412 <updated>1970-01-01T00:00:01+00:00</updated>
413 413 <published>1970-01-01T00:00:01+00:00</published>
414 414 <content type="xhtml">
415 415 <div xmlns="http://www.w3.org/1999/xhtml">
416 416 <pre xml:space="preserve">a</pre>
417 417 </div>
418 418 </content>
419 419 </entry>
420 420
421 421 </feed>
422 422 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/?style=atom'
423 423 200 Script output follows
424 424
425 425 <?xml version="1.0" encoding="ascii"?>
426 426 <feed xmlns="http://www.w3.org/2005/Atom">
427 427 <!-- Changelog -->
428 428 <id>http://*:$HGPORT1/t/a/</id> (glob)
429 429 <link rel="self" href="http://*:$HGPORT1/t/a/atom-log"/> (glob)
430 430 <link rel="alternate" href="http://*:$HGPORT1/t/a/"/> (glob)
431 431 <title>t/a Changelog</title>
432 432 <updated>1970-01-01T00:00:01+00:00</updated>
433 433
434 434 <entry>
435 435 <title>a</title>
436 436 <id>http://*:$HGPORT1/t/a/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id> (glob)
437 437 <link href="http://*:$HGPORT1/t/a/rev/8580ff50825a"/> (glob)
438 438 <author>
439 439 <name>test</name>
440 440 <email>&#116;&#101;&#115;&#116;</email>
441 441 </author>
442 442 <updated>1970-01-01T00:00:01+00:00</updated>
443 443 <published>1970-01-01T00:00:01+00:00</published>
444 444 <content type="xhtml">
445 445 <div xmlns="http://www.w3.org/1999/xhtml">
446 446 <pre xml:space="preserve">a</pre>
447 447 </div>
448 448 </content>
449 449 </entry>
450 450
451 451 </feed>
452 452 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/file/tip/a?style=raw'
453 453 200 Script output follows
454 454
455 455 a
456 456
457 457 Test [paths] '*' extension
458 458
459 459 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/coll/?style=raw'
460 460 200 Script output follows
461 461
462 462
463 463 /coll/a/
464 464 /coll/a/.hg/patches/
465 465 /coll/b/
466 466 /coll/c/
467 467
468 468 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/coll/a/file/tip/a?style=raw'
469 469 200 Script output follows
470 470
471 471 a
472 472
473 473 est [paths] '**' extension
474 474
475 475 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/rcoll/?style=raw'
476 476 200 Script output follows
477 477
478 478
479 479 /rcoll/a/
480 480 /rcoll/a/.hg/patches/
481 481 /rcoll/b/
482 482 /rcoll/b/d/
483 483 /rcoll/c/
484 484
485 485 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/rcoll/b/d/file/tip/d?style=raw'
486 486 200 Script output follows
487 487
488 488 d
489 489 $ "$TESTDIR/killdaemons.py"
490 490 $ cat > paths.conf <<EOF
491 491 > [paths]
492 492 > t/a = $root/a
493 493 > t/b = $root/b
494 494 > c = $root/c
495 495 > [web]
496 496 > descend=false
497 497 > EOF
498 498 $ hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf paths.conf \
499 499 > -A access-paths.log -E error-paths-3.log
500 500 $ cat hg.pid >> $DAEMON_PIDS
501 501
502 502 test descend = False
503 503
504 504 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=raw'
505 505 200 Script output follows
506 506
507 507
508 508 /c/
509 509
510 510 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/?style=raw'
511 511 200 Script output follows
512 512
513 513
514 514 /t/a/
515 515 /t/b/
516 516
517 517 $ "$TESTDIR/killdaemons.py"
518 518 $ cat > paths.conf <<EOF
519 519 > [paths]
520 520 > nostore = $root/nostore
521 521 > inexistent = $root/inexistent
522 522 > EOF
523 523 $ hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf paths.conf \
524 524 > -A access-paths.log -E error-paths-4.log
525 525 $ cat hg.pid >> $DAEMON_PIDS
526 526
527 527 test inexistent and inaccessible repo should be ignored silently
528 528
529 529 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/'
530 530 200 Script output follows
531 531
532 532 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
533 533 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
534 534 <head>
535 535 <link rel="icon" href="/static/hgicon.png" type="image/png" />
536 536 <meta name="robots" content="index, nofollow" />
537 537 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
538 538
539 539 <title>Mercurial repositories index</title>
540 540 </head>
541 541 <body>
542 542
543 543 <div class="container">
544 544 <div class="menu">
545 545 <a href="http://mercurial.selenic.com/">
546 546 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
547 547 </div>
548 548 <div class="main">
549 549 <h2>Mercurial Repositories</h2>
550 550
551 551 <table class="bigtable">
552 552 <tr>
553 553 <th><a href="?sort=name">Name</a></th>
554 554 <th><a href="?sort=description">Description</a></th>
555 555 <th><a href="?sort=contact">Contact</a></th>
556 556 <th><a href="?sort=lastchange">Last modified</a></th>
557 557 <th>&nbsp;</th>
558 558 </tr>
559 559
560 560 </table>
561 561 </div>
562 562 </div>
563 563
564 564
565 565 </body>
566 566 </html>
567 567
568 568 $ cat > collections.conf <<EOF
569 569 > [collections]
570 570 > $root=$root
571 571 > EOF
572 572 $ hg serve --config web.baseurl=http://hg.example.com:8080/ -p $HGPORT2 -d \
573 573 > --pid-file=hg.pid --webdir-conf collections.conf \
574 574 > -A access-collections.log -E error-collections.log
575 575 $ cat hg.pid >> $DAEMON_PIDS
576 576
577 577 collections: should succeed
578 578
579 579 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/?style=raw'
580 580 200 Script output follows
581 581
582 582
583 583 /a/
584 584 /a/.hg/patches/
585 585 /b/
586 586 /c/
587 587
588 588 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/file/tip/a?style=raw'
589 589 200 Script output follows
590 590
591 591 a
592 592 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/b/file/tip/b?style=raw'
593 593 200 Script output follows
594 594
595 595 b
596 596 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/c/file/tip/c?style=raw'
597 597 200 Script output follows
598 598
599 599 c
600 600
601 601 atom-log with basedir /
602 602
603 603 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/atom-log' | grep '<link'
604 604 <link rel="self" href="http://hg.example.com:8080/a/atom-log"/>
605 605 <link rel="alternate" href="http://hg.example.com:8080/a/"/>
606 606 <link href="http://hg.example.com:8080/a/rev/8580ff50825a"/>
607 607
608 608 rss-log with basedir /
609 609
610 610 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/rss-log' | grep '<guid'
611 611 <guid isPermaLink="true">http://hg.example.com:8080/a/rev/8580ff50825a</guid>
612 612 $ "$TESTDIR/killdaemons.py"
613 613 $ hg serve --config web.baseurl=http://hg.example.com:8080/foo/ -p $HGPORT2 -d \
614 614 > --pid-file=hg.pid --webdir-conf collections.conf \
615 615 > -A access-collections-2.log -E error-collections-2.log
616 616 $ cat hg.pid >> $DAEMON_PIDS
617 617
618 618 atom-log with basedir /foo/
619 619
620 620 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/atom-log' | grep '<link'
621 621 <link rel="self" href="http://hg.example.com:8080/foo/a/atom-log"/>
622 622 <link rel="alternate" href="http://hg.example.com:8080/foo/a/"/>
623 623 <link href="http://hg.example.com:8080/foo/a/rev/8580ff50825a"/>
624 624
625 625 rss-log with basedir /foo/
626 626
627 627 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/rss-log' | grep '<guid'
628 628 <guid isPermaLink="true">http://hg.example.com:8080/foo/a/rev/8580ff50825a</guid>
629 629
630 630 paths errors 1
631 631
632 632 $ cat error-paths-1.log
633 633
634 634 paths errors 2
635 635
636 636 $ cat error-paths-2.log
637 637
638 638 paths errors 3
639 639
640 640 $ cat error-paths-3.log
641 641
642 642 collections errors
643 643
644 644 $ cat error-collections.log
645 645
646 646 collections errors 2
647 647
648 648 $ cat error-collections-2.log
@@ -1,76 +1,76 b''
1 1 Tests whether or not hgwebdir properly handles various symlink topologies.
2 2
3 3 $ "$TESTDIR/hghave" symlink || exit 80
4 4 $ hg init a
5 5 $ echo a > a/a
6 6 $ hg --cwd a ci -Ama -d'1 0'
7 7 adding a
8 8 $ mkdir webdir
9 9 $ cd webdir
10 10 $ hg init b
11 11 $ echo b > b/b
12 12 $ hg --cwd b ci -Amb -d'2 0'
13 13 adding b
14 14 $ hg init c
15 15 $ echo c > c/c
16 16 $ hg --cwd c ci -Amc -d'3 0'
17 17 adding c
18 18 $ ln -s ../a al
19 19 $ ln -s ../webdir circle
20 20 $ root=`pwd`
21 21 $ cd ..
22 22 $ cat > collections.conf <<EOF
23 23 > [collections]
24 24 > $root=$root
25 25 > EOF
26 26 $ hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf collections.conf \
27 27 > -A access-collections.log -E error-collections.log
28 28 $ cat hg.pid >> $DAEMON_PIDS
29 29
30 30 should succeed
31 31
32 32 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/?style=raw'
33 33 200 Script output follows
34 34
35 35
36 36 /al/
37 37 /b/
38 38 /c/
39 39
40 40 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/al/file/tip/a?style=raw'
41 41 200 Script output follows
42 42
43 43 a
44 44 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/b/file/tip/b?style=raw'
45 45 200 Script output follows
46 46
47 47 b
48 48 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/c/file/tip/c?style=raw'
49 49 200 Script output follows
50 50
51 51 c
52 52
53 53 should fail
54 54
55 55 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/circle/al/file/tip/a?style=raw'
56 56 404 Not Found
57 57
58 58
59 error: repository circle not found
59 error: repository circle/al/file/tip/a not found
60 60 [1]
61 61 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/circle/b/file/tip/a?style=raw'
62 62 404 Not Found
63 63
64 64
65 error: repository circle not found
65 error: repository circle/b/file/tip/a not found
66 66 [1]
67 67 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/circle/c/file/tip/a?style=raw'
68 68 404 Not Found
69 69
70 70
71 error: repository circle not found
71 error: repository circle/c/file/tip/a not found
72 72 [1]
73 73
74 74 collections errors
75 75
76 76 $ cat error-collections.log
General Comments 0
You need to be logged in to leave comments. Login now