##// END OF EJS Templates
hgweb: recurse down collections only if ** in [paths]...
Benoit Allard -
r7523:e60aaae8 default
parent child Browse files
Show More
@@ -1,63 +1,67 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # An example CGI script to export multiple hgweb repos, edit as necessary
4 4
5 5 # adjust python path if not a system-wide install:
6 6 #import sys
7 7 #sys.path.insert(0, "/path/to/python/lib")
8 8
9 9 # enable importing on demand to reduce startup time
10 10 from mercurial import demandimport; demandimport.enable()
11 11
12 12 # Uncomment to send python tracebacks to the browser if an error occurs:
13 13 #import cgitb
14 14 #cgitb.enable()
15 15
16 16 # If you'd like to serve pages with UTF-8 instead of your default
17 17 # locale charset, you can do so by uncommenting the following lines.
18 18 # Note that this will cause your .hgrc files to be interpreted in
19 19 # UTF-8 and all your repo files to be displayed using UTF-8.
20 20 #
21 21 #import os
22 22 #os.environ["HGENCODING"] = "UTF-8"
23 23
24 24 from mercurial.hgweb.hgwebdir_mod import hgwebdir
25 25 import mercurial.hgweb.wsgicgi as wsgicgi
26 26
27 27 # The config file looks like this. You can have paths to individual
28 28 # repos, collections of repos in a directory tree, or both.
29 29 #
30 30 # [paths]
31 31 # virtual/path1 = /real/path1
32 32 # virtual/path2 = /real/path2
33 33 # virtual/root = /real/root/*
34 34 # / = /real/root2/*
35 # virtual/root2 = /real/root2/**
35 36 #
36 37 # [collections]
37 38 # /prefix/to/strip/off = /root/of/tree/full/of/repos
38 39 #
39 40 # paths example:
40 41 #
41 42 # * First two lines mount one repository into one virtual path, like
42 43 # '/real/path1' into 'virtual/path1'.
43 44 #
44 # * The third entry tells every mercurial repository found in
45 # '/real/root', recursively, should be mounted in 'virtual/root'. This
46 # format is preferred over the [collections] one, using absolute paths
47 # as configuration keys is not supported on every platform (including
48 # Windows).
45 # * The third entry mounts every mercurial repository found in '/real/root'
46 # in 'virtual/root'. This format is preferred over the [collections] one,
47 # since using absolute paths as configuration keys is not support on every
48 # platform (especially on Windows).
49 49 #
50 # * The last entry is a special case mounting all repositories in
50 # * The fourth entry is a special case mounting all repositories in
51 51 # /'real/root2' in the root of the virtual directory.
52 52 #
53 # * The fifth entry recursively finds all repositories under the real root,
54 # and mounts them using their relative path (to given real root) under the
55 # virtual root.
56 #
53 57 # collections example: say directory tree /foo contains repos /foo/bar,
54 58 # /foo/quux/baz. Give this config section:
55 59 # [collections]
56 60 # /foo = /foo
57 61 # Then repos will list as bar and quux/baz.
58 62 #
59 63 # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples
60 64 # or use a dictionary with entries like 'virtual/path': '/real/path'
61 65
62 66 application = hgwebdir('hgweb.config')
63 67 wsgicgi.launch(application)
@@ -1,323 +1,328 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
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os
10 10 from mercurial.i18n import _
11 11 from mercurial.repo import RepoError
12 12 from mercurial import ui, hg, util, templater, templatefilters
13 13 from common import ErrorResponse, get_mtime, staticfile, style_map, 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
18 18 # This is a stopgap
19 19 class hgwebdir(object):
20 20 def __init__(self, config, parentui=None):
21 21 def cleannames(items):
22 22 return [(util.pconvert(name).strip('/'), path)
23 23 for name, path in items]
24 24
25 25 self.parentui = parentui or ui.ui(report_untrusted=False,
26 26 interactive = False)
27 27 self.motd = None
28 28 self.style = 'paper'
29 29 self.stripecount = None
30 30 self.repos_sorted = ('name', False)
31 31 self._baseurl = None
32 32 if isinstance(config, (list, tuple)):
33 33 self.repos = cleannames(config)
34 34 self.repos_sorted = ('', False)
35 35 elif isinstance(config, dict):
36 36 self.repos = util.sort(cleannames(config.items()))
37 37 else:
38 38 if isinstance(config, util.configparser):
39 39 cp = config
40 40 else:
41 41 cp = util.configparser()
42 42 cp.read(config)
43 43 self.repos = []
44 44 if cp.has_section('web'):
45 45 if cp.has_option('web', 'motd'):
46 46 self.motd = cp.get('web', 'motd')
47 47 if cp.has_option('web', 'style'):
48 48 self.style = cp.get('web', 'style')
49 49 if cp.has_option('web', 'stripes'):
50 50 self.stripecount = int(cp.get('web', 'stripes'))
51 51 if cp.has_option('web', 'baseurl'):
52 52 self._baseurl = cp.get('web', 'baseurl')
53 53 if cp.has_section('paths'):
54 54 paths = cleannames(cp.items('paths'))
55 55 for prefix, root in paths:
56 56 roothead, roottail = os.path.split(root)
57 if roottail != '*':
57 # "foo = /bar/*" makes every subrepo of /bar/ to be
58 # mounted as foo/subrepo
59 # and "foo = /bar/**" does even recurse inside the
60 # subdirectories, remember to use it without working dir.
61 try:
62 recurse = {'*': False, '**': True}[roottail]
63 except KeyError:
58 64 self.repos.append((prefix, root))
59 65 continue
60 # "foo = /bar/*" makes every subrepo of /bar/ to be
61 # mounted as foo/subrepo
62 66 roothead = os.path.normpath(roothead)
63 for path in util.walkrepos(roothead, followsym=True):
67 for path in util.walkrepos(roothead, followsym=True,
68 recurse=recurse):
64 69 path = os.path.normpath(path)
65 70 name = util.pconvert(path[len(roothead):]).strip('/')
66 71 if prefix:
67 72 name = prefix + '/' + name
68 73 self.repos.append((name, path))
69 74 if cp.has_section('collections'):
70 75 for prefix, root in cp.items('collections'):
71 76 for path in util.walkrepos(root, followsym=True):
72 77 repo = os.path.normpath(path)
73 78 name = repo
74 79 if name.startswith(prefix):
75 80 name = name[len(prefix):]
76 81 self.repos.append((name.lstrip(os.sep), repo))
77 82 self.repos.sort()
78 83
79 84 def run(self):
80 85 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
81 86 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
82 87 import mercurial.hgweb.wsgicgi as wsgicgi
83 88 wsgicgi.launch(self)
84 89
85 90 def __call__(self, env, respond):
86 91 req = wsgirequest(env, respond)
87 92 return self.run_wsgi(req)
88 93
89 94 def read_allowed(self, ui, req):
90 95 """Check allow_read and deny_read config options of a repo's ui object
91 96 to determine user permissions. By default, with neither option set (or
92 97 both empty), allow all users to read the repo. There are two ways a
93 98 user can be denied read access: (1) deny_read is not empty, and the
94 99 user is unauthenticated or deny_read contains user (or *), and (2)
95 100 allow_read is not empty and the user is not in allow_read. Return True
96 101 if user is allowed to read the repo, else return False."""
97 102
98 103 user = req.env.get('REMOTE_USER')
99 104
100 105 deny_read = ui.configlist('web', 'deny_read', default=None, untrusted=True)
101 106 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
102 107 return False
103 108
104 109 allow_read = ui.configlist('web', 'allow_read', default=None, untrusted=True)
105 110 # by default, allow reading if no allow_read option has been set
106 111 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
107 112 return True
108 113
109 114 return False
110 115
111 116 def run_wsgi(self, req):
112 117
113 118 try:
114 119 try:
115 120
116 121 virtual = req.env.get("PATH_INFO", "").strip('/')
117 122 tmpl = self.templater(req)
118 123 ctype = tmpl('mimetype', encoding=util._encoding)
119 124 ctype = templater.stringify(ctype)
120 125
121 126 # a static file
122 127 if virtual.startswith('static/') or 'static' in req.form:
123 128 if virtual.startswith('static/'):
124 129 fname = virtual[7:]
125 130 else:
126 131 fname = req.form['static'][0]
127 132 static = templater.templatepath('static')
128 133 return staticfile(static, fname, req)
129 134
130 135 # top-level index
131 136 elif not virtual:
132 137 req.respond(HTTP_OK, ctype)
133 138 return self.makeindex(req, tmpl)
134 139
135 140 # nested indexes and hgwebs
136 141
137 142 repos = dict(self.repos)
138 143 while virtual:
139 144 real = repos.get(virtual)
140 145 if real:
141 146 req.env['REPO_NAME'] = virtual
142 147 try:
143 148 repo = hg.repository(self.parentui, real)
144 149 return hgweb(repo).run_wsgi(req)
145 150 except IOError, inst:
146 151 msg = inst.strerror
147 152 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
148 153 except RepoError, inst:
149 154 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
150 155
151 156 # browse subdirectories
152 157 subdir = virtual + '/'
153 158 if [r for r in repos if r.startswith(subdir)]:
154 159 req.respond(HTTP_OK, ctype)
155 160 return self.makeindex(req, tmpl, subdir)
156 161
157 162 up = virtual.rfind('/')
158 163 if up < 0:
159 164 break
160 165 virtual = virtual[:up]
161 166
162 167 # prefixes not found
163 168 req.respond(HTTP_NOT_FOUND, ctype)
164 169 return tmpl("notfound", repo=virtual)
165 170
166 171 except ErrorResponse, err:
167 172 req.respond(err.code, ctype)
168 173 return tmpl('error', error=err.message or '')
169 174 finally:
170 175 tmpl = None
171 176
172 177 def makeindex(self, req, tmpl, subdir=""):
173 178
174 179 def archivelist(ui, nodeid, url):
175 180 allowed = ui.configlist("web", "allow_archive", untrusted=True)
176 181 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
177 182 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
178 183 untrusted=True):
179 184 yield {"type" : i[0], "extension": i[1],
180 185 "node": nodeid, "url": url}
181 186
182 187 def entries(sortcolumn="", descending=False, subdir="", **map):
183 188 def sessionvars(**map):
184 189 fields = []
185 190 if 'style' in req.form:
186 191 style = req.form['style'][0]
187 192 if style != get('web', 'style', ''):
188 193 fields.append(('style', style))
189 194
190 195 separator = url[-1] == '?' and ';' or '?'
191 196 for name, value in fields:
192 197 yield dict(name=name, value=value, separator=separator)
193 198 separator = ';'
194 199
195 200 rows = []
196 201 parity = paritygen(self.stripecount)
197 202 for name, path in self.repos:
198 203 if not name.startswith(subdir):
199 204 continue
200 205 name = name[len(subdir):]
201 206
202 207 u = ui.ui(parentui=self.parentui)
203 208 try:
204 209 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
205 210 except Exception, e:
206 211 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
207 212 continue
208 213 def get(section, name, default=None):
209 214 return u.config(section, name, default, untrusted=True)
210 215
211 216 if u.configbool("web", "hidden", untrusted=True):
212 217 continue
213 218
214 219 if not self.read_allowed(u, req):
215 220 continue
216 221
217 222 parts = [name]
218 223 if 'PATH_INFO' in req.env:
219 224 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
220 225 if req.env['SCRIPT_NAME']:
221 226 parts.insert(0, req.env['SCRIPT_NAME'])
222 227 url = ('/'.join(parts).replace("//", "/")) + '/'
223 228
224 229 # update time with local timezone
225 230 try:
226 231 d = (get_mtime(path), util.makedate()[1])
227 232 except OSError:
228 233 continue
229 234
230 235 contact = get_contact(get)
231 236 description = get("web", "description", "")
232 237 name = get("web", "name", name)
233 238 row = dict(contact=contact or "unknown",
234 239 contact_sort=contact.upper() or "unknown",
235 240 name=name,
236 241 name_sort=name,
237 242 url=url,
238 243 description=description or "unknown",
239 244 description_sort=description.upper() or "unknown",
240 245 lastchange=d,
241 246 lastchange_sort=d[1]-d[0],
242 247 sessionvars=sessionvars,
243 248 archives=archivelist(u, "tip", url))
244 249 if (not sortcolumn
245 250 or (sortcolumn, descending) == self.repos_sorted):
246 251 # fast path for unsorted output
247 252 row['parity'] = parity.next()
248 253 yield row
249 254 else:
250 255 rows.append((row["%s_sort" % sortcolumn], row))
251 256 if rows:
252 257 rows.sort()
253 258 if descending:
254 259 rows.reverse()
255 260 for key, row in rows:
256 261 row['parity'] = parity.next()
257 262 yield row
258 263
259 264 sortable = ["name", "description", "contact", "lastchange"]
260 265 sortcolumn, descending = self.repos_sorted
261 266 if 'sort' in req.form:
262 267 sortcolumn = req.form['sort'][0]
263 268 descending = sortcolumn.startswith('-')
264 269 if descending:
265 270 sortcolumn = sortcolumn[1:]
266 271 if sortcolumn not in sortable:
267 272 sortcolumn = ""
268 273
269 274 sort = [("sort_%s" % column,
270 275 "%s%s" % ((not descending and column == sortcolumn)
271 276 and "-" or "", column))
272 277 for column in sortable]
273 278
274 279 if self._baseurl is not None:
275 280 req.env['SCRIPT_NAME'] = self._baseurl
276 281
277 282 return tmpl("index", entries=entries, subdir=subdir,
278 283 sortcolumn=sortcolumn, descending=descending,
279 284 **dict(sort))
280 285
281 286 def templater(self, req):
282 287
283 288 def header(**map):
284 289 yield tmpl('header', encoding=util._encoding, **map)
285 290
286 291 def footer(**map):
287 292 yield tmpl("footer", **map)
288 293
289 294 def motd(**map):
290 295 if self.motd is not None:
291 296 yield self.motd
292 297 else:
293 298 yield config('web', 'motd', '')
294 299
295 300 def config(section, name, default=None, untrusted=True):
296 301 return self.parentui.config(section, name, default, untrusted)
297 302
298 303 if self._baseurl is not None:
299 304 req.env['SCRIPT_NAME'] = self._baseurl
300 305
301 306 url = req.env.get('SCRIPT_NAME', '')
302 307 if not url.endswith('/'):
303 308 url += '/'
304 309
305 310 staticurl = config('web', 'staticurl') or url + 'static/'
306 311 if not staticurl.endswith('/'):
307 312 staticurl += '/'
308 313
309 314 style = self.style
310 315 if style is None:
311 316 style = config('web', 'style', '')
312 317 if 'style' in req.form:
313 318 style = req.form['style'][0]
314 319 if self.stripecount is None:
315 320 self.stripecount = int(config('web', 'stripes', 1))
316 321 mapfile = style_map(templater.templatepath(), style)
317 322 tmpl = templater.templater(mapfile, templatefilters.filters,
318 323 defaults={"header": header,
319 324 "footer": footer,
320 325 "motd": motd,
321 326 "url": url,
322 327 "staticurl": staticurl})
323 328 return tmpl
@@ -1,1983 +1,1988 b''
1 1 """
2 2 util.py - Mercurial utility functions and platform specfic implementations
3 3
4 4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
5 5 Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
7 7
8 8 This software may be used and distributed according to the terms
9 9 of the GNU General Public License, incorporated herein by reference.
10 10
11 11 This contains helper routines that are independent of the SCM core and hide
12 12 platform-specific details from the core.
13 13 """
14 14
15 15 from i18n import _
16 16 import cStringIO, errno, getpass, re, shutil, sys, tempfile, traceback
17 17 import os, stat, threading, time, calendar, ConfigParser, locale, glob, osutil
18 18 import imp
19 19
20 20 # Python compatibility
21 21
22 22 try:
23 23 set = set
24 24 frozenset = frozenset
25 25 except NameError:
26 26 from sets import Set as set, ImmutableSet as frozenset
27 27
28 28 _md5 = None
29 29 def md5(s):
30 30 global _md5
31 31 if _md5 is None:
32 32 try:
33 33 import hashlib
34 34 _md5 = hashlib.md5
35 35 except ImportError:
36 36 import md5
37 37 _md5 = md5.md5
38 38 return _md5(s)
39 39
40 40 _sha1 = None
41 41 def sha1(s):
42 42 global _sha1
43 43 if _sha1 is None:
44 44 try:
45 45 import hashlib
46 46 _sha1 = hashlib.sha1
47 47 except ImportError:
48 48 import sha
49 49 _sha1 = sha.sha
50 50 return _sha1(s)
51 51
52 52 try:
53 53 import subprocess
54 54 subprocess.Popen # trigger ImportError early
55 55 closefds = os.name == 'posix'
56 56 def popen2(cmd, mode='t', bufsize=-1):
57 57 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
58 58 close_fds=closefds,
59 59 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
60 60 return p.stdin, p.stdout
61 61 def popen3(cmd, mode='t', bufsize=-1):
62 62 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
63 63 close_fds=closefds,
64 64 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
65 65 stderr=subprocess.PIPE)
66 66 return p.stdin, p.stdout, p.stderr
67 67 def Popen3(cmd, capturestderr=False, bufsize=-1):
68 68 stderr = capturestderr and subprocess.PIPE or None
69 69 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
70 70 close_fds=closefds,
71 71 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
72 72 stderr=stderr)
73 73 p.fromchild = p.stdout
74 74 p.tochild = p.stdin
75 75 p.childerr = p.stderr
76 76 return p
77 77 except ImportError:
78 78 subprocess = None
79 79 from popen2 import Popen3
80 80 popen2 = os.popen2
81 81 popen3 = os.popen3
82 82
83 83
84 84 _encodingfixup = {'646': 'ascii', 'ANSI_X3.4-1968': 'ascii'}
85 85
86 86 try:
87 87 _encoding = os.environ.get("HGENCODING")
88 88 if sys.platform == 'darwin' and not _encoding:
89 89 # On darwin, getpreferredencoding ignores the locale environment and
90 90 # always returns mac-roman. We override this if the environment is
91 91 # not C (has been customized by the user).
92 92 locale.setlocale(locale.LC_CTYPE, '')
93 93 _encoding = locale.getlocale()[1]
94 94 if not _encoding:
95 95 _encoding = locale.getpreferredencoding() or 'ascii'
96 96 _encoding = _encodingfixup.get(_encoding, _encoding)
97 97 except locale.Error:
98 98 _encoding = 'ascii'
99 99 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
100 100 _fallbackencoding = 'ISO-8859-1'
101 101
102 102 def tolocal(s):
103 103 """
104 104 Convert a string from internal UTF-8 to local encoding
105 105
106 106 All internal strings should be UTF-8 but some repos before the
107 107 implementation of locale support may contain latin1 or possibly
108 108 other character sets. We attempt to decode everything strictly
109 109 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
110 110 replace unknown characters.
111 111 """
112 112 for e in ('UTF-8', _fallbackencoding):
113 113 try:
114 114 u = s.decode(e) # attempt strict decoding
115 115 return u.encode(_encoding, "replace")
116 116 except LookupError, k:
117 117 raise Abort(_("%s, please check your locale settings") % k)
118 118 except UnicodeDecodeError:
119 119 pass
120 120 u = s.decode("utf-8", "replace") # last ditch
121 121 return u.encode(_encoding, "replace")
122 122
123 123 def fromlocal(s):
124 124 """
125 125 Convert a string from the local character encoding to UTF-8
126 126
127 127 We attempt to decode strings using the encoding mode set by
128 128 HGENCODINGMODE, which defaults to 'strict'. In this mode, unknown
129 129 characters will cause an error message. Other modes include
130 130 'replace', which replaces unknown characters with a special
131 131 Unicode character, and 'ignore', which drops the character.
132 132 """
133 133 try:
134 134 return s.decode(_encoding, _encodingmode).encode("utf-8")
135 135 except UnicodeDecodeError, inst:
136 136 sub = s[max(0, inst.start-10):inst.start+10]
137 137 raise Abort("decoding near '%s': %s!" % (sub, inst))
138 138 except LookupError, k:
139 139 raise Abort(_("%s, please check your locale settings") % k)
140 140
141 141 def locallen(s):
142 142 """Find the length in characters of a local string"""
143 143 return len(s.decode(_encoding, "replace"))
144 144
145 145 # used by parsedate
146 146 defaultdateformats = (
147 147 '%Y-%m-%d %H:%M:%S',
148 148 '%Y-%m-%d %I:%M:%S%p',
149 149 '%Y-%m-%d %H:%M',
150 150 '%Y-%m-%d %I:%M%p',
151 151 '%Y-%m-%d',
152 152 '%m-%d',
153 153 '%m/%d',
154 154 '%m/%d/%y',
155 155 '%m/%d/%Y',
156 156 '%a %b %d %H:%M:%S %Y',
157 157 '%a %b %d %I:%M:%S%p %Y',
158 158 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
159 159 '%b %d %H:%M:%S %Y',
160 160 '%b %d %I:%M:%S%p %Y',
161 161 '%b %d %H:%M:%S',
162 162 '%b %d %I:%M:%S%p',
163 163 '%b %d %H:%M',
164 164 '%b %d %I:%M%p',
165 165 '%b %d %Y',
166 166 '%b %d',
167 167 '%H:%M:%S',
168 168 '%I:%M:%SP',
169 169 '%H:%M',
170 170 '%I:%M%p',
171 171 )
172 172
173 173 extendeddateformats = defaultdateformats + (
174 174 "%Y",
175 175 "%Y-%m",
176 176 "%b",
177 177 "%b %Y",
178 178 )
179 179
180 180 class SignalInterrupt(Exception):
181 181 """Exception raised on SIGTERM and SIGHUP."""
182 182
183 183 # differences from SafeConfigParser:
184 184 # - case-sensitive keys
185 185 # - allows values that are not strings (this means that you may not
186 186 # be able to save the configuration to a file)
187 187 class configparser(ConfigParser.SafeConfigParser):
188 188 def optionxform(self, optionstr):
189 189 return optionstr
190 190
191 191 def set(self, section, option, value):
192 192 return ConfigParser.ConfigParser.set(self, section, option, value)
193 193
194 194 def _interpolate(self, section, option, rawval, vars):
195 195 if not isinstance(rawval, basestring):
196 196 return rawval
197 197 return ConfigParser.SafeConfigParser._interpolate(self, section,
198 198 option, rawval, vars)
199 199
200 200 def cachefunc(func):
201 201 '''cache the result of function calls'''
202 202 # XXX doesn't handle keywords args
203 203 cache = {}
204 204 if func.func_code.co_argcount == 1:
205 205 # we gain a small amount of time because
206 206 # we don't need to pack/unpack the list
207 207 def f(arg):
208 208 if arg not in cache:
209 209 cache[arg] = func(arg)
210 210 return cache[arg]
211 211 else:
212 212 def f(*args):
213 213 if args not in cache:
214 214 cache[args] = func(*args)
215 215 return cache[args]
216 216
217 217 return f
218 218
219 219 def pipefilter(s, cmd):
220 220 '''filter string S through command CMD, returning its output'''
221 221 (pin, pout) = popen2(cmd, 'b')
222 222 def writer():
223 223 try:
224 224 pin.write(s)
225 225 pin.close()
226 226 except IOError, inst:
227 227 if inst.errno != errno.EPIPE:
228 228 raise
229 229
230 230 # we should use select instead on UNIX, but this will work on most
231 231 # systems, including Windows
232 232 w = threading.Thread(target=writer)
233 233 w.start()
234 234 f = pout.read()
235 235 pout.close()
236 236 w.join()
237 237 return f
238 238
239 239 def tempfilter(s, cmd):
240 240 '''filter string S through a pair of temporary files with CMD.
241 241 CMD is used as a template to create the real command to be run,
242 242 with the strings INFILE and OUTFILE replaced by the real names of
243 243 the temporary files generated.'''
244 244 inname, outname = None, None
245 245 try:
246 246 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
247 247 fp = os.fdopen(infd, 'wb')
248 248 fp.write(s)
249 249 fp.close()
250 250 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
251 251 os.close(outfd)
252 252 cmd = cmd.replace('INFILE', inname)
253 253 cmd = cmd.replace('OUTFILE', outname)
254 254 code = os.system(cmd)
255 255 if sys.platform == 'OpenVMS' and code & 1:
256 256 code = 0
257 257 if code: raise Abort(_("command '%s' failed: %s") %
258 258 (cmd, explain_exit(code)))
259 259 return open(outname, 'rb').read()
260 260 finally:
261 261 try:
262 262 if inname: os.unlink(inname)
263 263 except: pass
264 264 try:
265 265 if outname: os.unlink(outname)
266 266 except: pass
267 267
268 268 filtertable = {
269 269 'tempfile:': tempfilter,
270 270 'pipe:': pipefilter,
271 271 }
272 272
273 273 def filter(s, cmd):
274 274 "filter a string through a command that transforms its input to its output"
275 275 for name, fn in filtertable.iteritems():
276 276 if cmd.startswith(name):
277 277 return fn(s, cmd[len(name):].lstrip())
278 278 return pipefilter(s, cmd)
279 279
280 280 def binary(s):
281 281 """return true if a string is binary data"""
282 282 if s and '\0' in s:
283 283 return True
284 284 return False
285 285
286 286 def unique(g):
287 287 """return the uniq elements of iterable g"""
288 288 return dict.fromkeys(g).keys()
289 289
290 290 def sort(l):
291 291 if not isinstance(l, list):
292 292 l = list(l)
293 293 l.sort()
294 294 return l
295 295
296 296 def increasingchunks(source, min=1024, max=65536):
297 297 '''return no less than min bytes per chunk while data remains,
298 298 doubling min after each chunk until it reaches max'''
299 299 def log2(x):
300 300 if not x:
301 301 return 0
302 302 i = 0
303 303 while x:
304 304 x >>= 1
305 305 i += 1
306 306 return i - 1
307 307
308 308 buf = []
309 309 blen = 0
310 310 for chunk in source:
311 311 buf.append(chunk)
312 312 blen += len(chunk)
313 313 if blen >= min:
314 314 if min < max:
315 315 min = min << 1
316 316 nmin = 1 << log2(blen)
317 317 if nmin > min:
318 318 min = nmin
319 319 if min > max:
320 320 min = max
321 321 yield ''.join(buf)
322 322 blen = 0
323 323 buf = []
324 324 if buf:
325 325 yield ''.join(buf)
326 326
327 327 class Abort(Exception):
328 328 """Raised if a command needs to print an error and exit."""
329 329
330 330 class UnexpectedOutput(Abort):
331 331 """Raised to print an error with part of output and exit."""
332 332
333 333 def always(fn): return True
334 334 def never(fn): return False
335 335
336 336 def expand_glob(pats):
337 337 '''On Windows, expand the implicit globs in a list of patterns'''
338 338 if os.name != 'nt':
339 339 return list(pats)
340 340 ret = []
341 341 for p in pats:
342 342 kind, name = patkind(p, None)
343 343 if kind is None:
344 344 globbed = glob.glob(name)
345 345 if globbed:
346 346 ret.extend(globbed)
347 347 continue
348 348 # if we couldn't expand the glob, just keep it around
349 349 ret.append(p)
350 350 return ret
351 351
352 352 def patkind(name, default):
353 353 """Split a string into an optional pattern kind prefix and the
354 354 actual pattern."""
355 355 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
356 356 if name.startswith(prefix + ':'): return name.split(':', 1)
357 357 return default, name
358 358
359 359 def globre(pat, head='^', tail='$'):
360 360 "convert a glob pattern into a regexp"
361 361 i, n = 0, len(pat)
362 362 res = ''
363 363 group = 0
364 364 def peek(): return i < n and pat[i]
365 365 while i < n:
366 366 c = pat[i]
367 367 i = i+1
368 368 if c == '*':
369 369 if peek() == '*':
370 370 i += 1
371 371 res += '.*'
372 372 else:
373 373 res += '[^/]*'
374 374 elif c == '?':
375 375 res += '.'
376 376 elif c == '[':
377 377 j = i
378 378 if j < n and pat[j] in '!]':
379 379 j += 1
380 380 while j < n and pat[j] != ']':
381 381 j += 1
382 382 if j >= n:
383 383 res += '\\['
384 384 else:
385 385 stuff = pat[i:j].replace('\\','\\\\')
386 386 i = j + 1
387 387 if stuff[0] == '!':
388 388 stuff = '^' + stuff[1:]
389 389 elif stuff[0] == '^':
390 390 stuff = '\\' + stuff
391 391 res = '%s[%s]' % (res, stuff)
392 392 elif c == '{':
393 393 group += 1
394 394 res += '(?:'
395 395 elif c == '}' and group:
396 396 res += ')'
397 397 group -= 1
398 398 elif c == ',' and group:
399 399 res += '|'
400 400 elif c == '\\':
401 401 p = peek()
402 402 if p:
403 403 i += 1
404 404 res += re.escape(p)
405 405 else:
406 406 res += re.escape(c)
407 407 else:
408 408 res += re.escape(c)
409 409 return head + res + tail
410 410
411 411 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
412 412
413 413 def pathto(root, n1, n2):
414 414 '''return the relative path from one place to another.
415 415 root should use os.sep to separate directories
416 416 n1 should use os.sep to separate directories
417 417 n2 should use "/" to separate directories
418 418 returns an os.sep-separated path.
419 419
420 420 If n1 is a relative path, it's assumed it's
421 421 relative to root.
422 422 n2 should always be relative to root.
423 423 '''
424 424 if not n1: return localpath(n2)
425 425 if os.path.isabs(n1):
426 426 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
427 427 return os.path.join(root, localpath(n2))
428 428 n2 = '/'.join((pconvert(root), n2))
429 429 a, b = splitpath(n1), n2.split('/')
430 430 a.reverse()
431 431 b.reverse()
432 432 while a and b and a[-1] == b[-1]:
433 433 a.pop()
434 434 b.pop()
435 435 b.reverse()
436 436 return os.sep.join((['..'] * len(a)) + b) or '.'
437 437
438 438 def canonpath(root, cwd, myname):
439 439 """return the canonical path of myname, given cwd and root"""
440 440 if root == os.sep:
441 441 rootsep = os.sep
442 442 elif endswithsep(root):
443 443 rootsep = root
444 444 else:
445 445 rootsep = root + os.sep
446 446 name = myname
447 447 if not os.path.isabs(name):
448 448 name = os.path.join(root, cwd, name)
449 449 name = os.path.normpath(name)
450 450 audit_path = path_auditor(root)
451 451 if name != rootsep and name.startswith(rootsep):
452 452 name = name[len(rootsep):]
453 453 audit_path(name)
454 454 return pconvert(name)
455 455 elif name == root:
456 456 return ''
457 457 else:
458 458 # Determine whether `name' is in the hierarchy at or beneath `root',
459 459 # by iterating name=dirname(name) until that causes no change (can't
460 460 # check name == '/', because that doesn't work on windows). For each
461 461 # `name', compare dev/inode numbers. If they match, the list `rel'
462 462 # holds the reversed list of components making up the relative file
463 463 # name we want.
464 464 root_st = os.stat(root)
465 465 rel = []
466 466 while True:
467 467 try:
468 468 name_st = os.stat(name)
469 469 except OSError:
470 470 break
471 471 if samestat(name_st, root_st):
472 472 if not rel:
473 473 # name was actually the same as root (maybe a symlink)
474 474 return ''
475 475 rel.reverse()
476 476 name = os.path.join(*rel)
477 477 audit_path(name)
478 478 return pconvert(name)
479 479 dirname, basename = os.path.split(name)
480 480 rel.append(basename)
481 481 if dirname == name:
482 482 break
483 483 name = dirname
484 484
485 485 raise Abort('%s not under root' % myname)
486 486
487 487 def matcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None, dflt_pat='glob'):
488 488 """build a function to match a set of file patterns
489 489
490 490 arguments:
491 491 canonroot - the canonical root of the tree you're matching against
492 492 cwd - the current working directory, if relevant
493 493 names - patterns to find
494 494 inc - patterns to include
495 495 exc - patterns to exclude
496 496 dflt_pat - if a pattern in names has no explicit type, assume this one
497 497 src - where these patterns came from (e.g. .hgignore)
498 498
499 499 a pattern is one of:
500 500 'glob:<glob>' - a glob relative to cwd
501 501 're:<regexp>' - a regular expression
502 502 'path:<path>' - a path relative to canonroot
503 503 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
504 504 'relpath:<path>' - a path relative to cwd
505 505 'relre:<regexp>' - a regexp that doesn't have to match the start of a name
506 506 '<something>' - one of the cases above, selected by the dflt_pat argument
507 507
508 508 returns:
509 509 a 3-tuple containing
510 510 - list of roots (places where one should start a recursive walk of the fs);
511 511 this often matches the explicit non-pattern names passed in, but also
512 512 includes the initial part of glob: patterns that has no glob characters
513 513 - a bool match(filename) function
514 514 - a bool indicating if any patterns were passed in
515 515 """
516 516
517 517 # a common case: no patterns at all
518 518 if not names and not inc and not exc:
519 519 return [], always, False
520 520
521 521 def contains_glob(name):
522 522 for c in name:
523 523 if c in _globchars: return True
524 524 return False
525 525
526 526 def regex(kind, name, tail):
527 527 '''convert a pattern into a regular expression'''
528 528 if not name:
529 529 return ''
530 530 if kind == 're':
531 531 return name
532 532 elif kind == 'path':
533 533 return '^' + re.escape(name) + '(?:/|$)'
534 534 elif kind == 'relglob':
535 535 return globre(name, '(?:|.*/)', tail)
536 536 elif kind == 'relpath':
537 537 return re.escape(name) + '(?:/|$)'
538 538 elif kind == 'relre':
539 539 if name.startswith('^'):
540 540 return name
541 541 return '.*' + name
542 542 return globre(name, '', tail)
543 543
544 544 def matchfn(pats, tail):
545 545 """build a matching function from a set of patterns"""
546 546 if not pats:
547 547 return
548 548 try:
549 549 pat = '(?:%s)' % '|'.join([regex(k, p, tail) for (k, p) in pats])
550 550 if len(pat) > 20000:
551 551 raise OverflowError()
552 552 return re.compile(pat).match
553 553 except OverflowError:
554 554 # We're using a Python with a tiny regex engine and we
555 555 # made it explode, so we'll divide the pattern list in two
556 556 # until it works
557 557 l = len(pats)
558 558 if l < 2:
559 559 raise
560 560 a, b = matchfn(pats[:l//2], tail), matchfn(pats[l//2:], tail)
561 561 return lambda s: a(s) or b(s)
562 562 except re.error:
563 563 for k, p in pats:
564 564 try:
565 565 re.compile('(?:%s)' % regex(k, p, tail))
566 566 except re.error:
567 567 if src:
568 568 raise Abort("%s: invalid pattern (%s): %s" %
569 569 (src, k, p))
570 570 else:
571 571 raise Abort("invalid pattern (%s): %s" % (k, p))
572 572 raise Abort("invalid pattern")
573 573
574 574 def globprefix(pat):
575 575 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
576 576 root = []
577 577 for p in pat.split('/'):
578 578 if contains_glob(p): break
579 579 root.append(p)
580 580 return '/'.join(root) or '.'
581 581
582 582 def normalizepats(names, default):
583 583 pats = []
584 584 roots = []
585 585 anypats = False
586 586 for kind, name in [patkind(p, default) for p in names]:
587 587 if kind in ('glob', 'relpath'):
588 588 name = canonpath(canonroot, cwd, name)
589 589 elif kind in ('relglob', 'path'):
590 590 name = normpath(name)
591 591
592 592 pats.append((kind, name))
593 593
594 594 if kind in ('glob', 're', 'relglob', 'relre'):
595 595 anypats = True
596 596
597 597 if kind == 'glob':
598 598 root = globprefix(name)
599 599 roots.append(root)
600 600 elif kind in ('relpath', 'path'):
601 601 roots.append(name or '.')
602 602 elif kind == 'relglob':
603 603 roots.append('.')
604 604 return roots, pats, anypats
605 605
606 606 roots, pats, anypats = normalizepats(names, dflt_pat)
607 607
608 608 patmatch = matchfn(pats, '$') or always
609 609 incmatch = always
610 610 if inc:
611 611 dummy, inckinds, dummy = normalizepats(inc, 'glob')
612 612 incmatch = matchfn(inckinds, '(?:/|$)')
613 613 excmatch = never
614 614 if exc:
615 615 dummy, exckinds, dummy = normalizepats(exc, 'glob')
616 616 excmatch = matchfn(exckinds, '(?:/|$)')
617 617
618 618 if not names and inc and not exc:
619 619 # common case: hgignore patterns
620 620 match = incmatch
621 621 else:
622 622 match = lambda fn: incmatch(fn) and not excmatch(fn) and patmatch(fn)
623 623
624 624 return (roots, match, (inc or exc or anypats) and True)
625 625
626 626 _hgexecutable = None
627 627
628 628 def main_is_frozen():
629 629 """return True if we are a frozen executable.
630 630
631 631 The code supports py2exe (most common, Windows only) and tools/freeze
632 632 (portable, not much used).
633 633 """
634 634 return (hasattr(sys, "frozen") or # new py2exe
635 635 hasattr(sys, "importers") or # old py2exe
636 636 imp.is_frozen("__main__")) # tools/freeze
637 637
638 638 def hgexecutable():
639 639 """return location of the 'hg' executable.
640 640
641 641 Defaults to $HG or 'hg' in the search path.
642 642 """
643 643 if _hgexecutable is None:
644 644 hg = os.environ.get('HG')
645 645 if hg:
646 646 set_hgexecutable(hg)
647 647 elif main_is_frozen():
648 648 set_hgexecutable(sys.executable)
649 649 else:
650 650 set_hgexecutable(find_exe('hg', 'hg'))
651 651 return _hgexecutable
652 652
653 653 def set_hgexecutable(path):
654 654 """set location of the 'hg' executable"""
655 655 global _hgexecutable
656 656 _hgexecutable = path
657 657
658 658 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
659 659 '''enhanced shell command execution.
660 660 run with environment maybe modified, maybe in different dir.
661 661
662 662 if command fails and onerr is None, return status. if ui object,
663 663 print error message and return status, else raise onerr object as
664 664 exception.'''
665 665 def py2shell(val):
666 666 'convert python object into string that is useful to shell'
667 667 if val in (None, False):
668 668 return '0'
669 669 if val == True:
670 670 return '1'
671 671 return str(val)
672 672 oldenv = {}
673 673 for k in environ:
674 674 oldenv[k] = os.environ.get(k)
675 675 if cwd is not None:
676 676 oldcwd = os.getcwd()
677 677 origcmd = cmd
678 678 if os.name == 'nt':
679 679 cmd = '"%s"' % cmd
680 680 try:
681 681 for k, v in environ.iteritems():
682 682 os.environ[k] = py2shell(v)
683 683 os.environ['HG'] = hgexecutable()
684 684 if cwd is not None and oldcwd != cwd:
685 685 os.chdir(cwd)
686 686 rc = os.system(cmd)
687 687 if sys.platform == 'OpenVMS' and rc & 1:
688 688 rc = 0
689 689 if rc and onerr:
690 690 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
691 691 explain_exit(rc)[0])
692 692 if errprefix:
693 693 errmsg = '%s: %s' % (errprefix, errmsg)
694 694 try:
695 695 onerr.warn(errmsg + '\n')
696 696 except AttributeError:
697 697 raise onerr(errmsg)
698 698 return rc
699 699 finally:
700 700 for k, v in oldenv.iteritems():
701 701 if v is None:
702 702 del os.environ[k]
703 703 else:
704 704 os.environ[k] = v
705 705 if cwd is not None and oldcwd != cwd:
706 706 os.chdir(oldcwd)
707 707
708 708 class SignatureError:
709 709 pass
710 710
711 711 def checksignature(func):
712 712 '''wrap a function with code to check for calling errors'''
713 713 def check(*args, **kwargs):
714 714 try:
715 715 return func(*args, **kwargs)
716 716 except TypeError:
717 717 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
718 718 raise SignatureError
719 719 raise
720 720
721 721 return check
722 722
723 723 # os.path.lexists is not available on python2.3
724 724 def lexists(filename):
725 725 "test whether a file with this name exists. does not follow symlinks"
726 726 try:
727 727 os.lstat(filename)
728 728 except:
729 729 return False
730 730 return True
731 731
732 732 def rename(src, dst):
733 733 """forcibly rename a file"""
734 734 try:
735 735 os.rename(src, dst)
736 736 except OSError, err: # FIXME: check err (EEXIST ?)
737 737 # on windows, rename to existing file is not allowed, so we
738 738 # must delete destination first. but if file is open, unlink
739 739 # schedules it for delete but does not delete it. rename
740 740 # happens immediately even for open files, so we create
741 741 # temporary file, delete it, rename destination to that name,
742 742 # then delete that. then rename is safe to do.
743 743 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
744 744 os.close(fd)
745 745 os.unlink(temp)
746 746 os.rename(dst, temp)
747 747 os.unlink(temp)
748 748 os.rename(src, dst)
749 749
750 750 def unlink(f):
751 751 """unlink and remove the directory if it is empty"""
752 752 os.unlink(f)
753 753 # try removing directories that might now be empty
754 754 try:
755 755 os.removedirs(os.path.dirname(f))
756 756 except OSError:
757 757 pass
758 758
759 759 def copyfile(src, dest):
760 760 "copy a file, preserving mode"
761 761 if os.path.islink(src):
762 762 try:
763 763 os.unlink(dest)
764 764 except:
765 765 pass
766 766 os.symlink(os.readlink(src), dest)
767 767 else:
768 768 try:
769 769 shutil.copyfile(src, dest)
770 770 shutil.copymode(src, dest)
771 771 except shutil.Error, inst:
772 772 raise Abort(str(inst))
773 773
774 774 def copyfiles(src, dst, hardlink=None):
775 775 """Copy a directory tree using hardlinks if possible"""
776 776
777 777 if hardlink is None:
778 778 hardlink = (os.stat(src).st_dev ==
779 779 os.stat(os.path.dirname(dst)).st_dev)
780 780
781 781 if os.path.isdir(src):
782 782 os.mkdir(dst)
783 783 for name, kind in osutil.listdir(src):
784 784 srcname = os.path.join(src, name)
785 785 dstname = os.path.join(dst, name)
786 786 copyfiles(srcname, dstname, hardlink)
787 787 else:
788 788 if hardlink:
789 789 try:
790 790 os_link(src, dst)
791 791 except (IOError, OSError):
792 792 hardlink = False
793 793 shutil.copy(src, dst)
794 794 else:
795 795 shutil.copy(src, dst)
796 796
797 797 class path_auditor(object):
798 798 '''ensure that a filesystem path contains no banned components.
799 799 the following properties of a path are checked:
800 800
801 801 - under top-level .hg
802 802 - starts at the root of a windows drive
803 803 - contains ".."
804 804 - traverses a symlink (e.g. a/symlink_here/b)
805 805 - inside a nested repository'''
806 806
807 807 def __init__(self, root):
808 808 self.audited = set()
809 809 self.auditeddir = set()
810 810 self.root = root
811 811
812 812 def __call__(self, path):
813 813 if path in self.audited:
814 814 return
815 815 normpath = os.path.normcase(path)
816 816 parts = splitpath(normpath)
817 817 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
818 818 or os.pardir in parts):
819 819 raise Abort(_("path contains illegal component: %s") % path)
820 820 def check(prefix):
821 821 curpath = os.path.join(self.root, prefix)
822 822 try:
823 823 st = os.lstat(curpath)
824 824 except OSError, err:
825 825 # EINVAL can be raised as invalid path syntax under win32.
826 826 # They must be ignored for patterns can be checked too.
827 827 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
828 828 raise
829 829 else:
830 830 if stat.S_ISLNK(st.st_mode):
831 831 raise Abort(_('path %r traverses symbolic link %r') %
832 832 (path, prefix))
833 833 elif (stat.S_ISDIR(st.st_mode) and
834 834 os.path.isdir(os.path.join(curpath, '.hg'))):
835 835 raise Abort(_('path %r is inside repo %r') %
836 836 (path, prefix))
837 837 parts.pop()
838 838 prefixes = []
839 839 for n in range(len(parts)):
840 840 prefix = os.sep.join(parts)
841 841 if prefix in self.auditeddir:
842 842 break
843 843 check(prefix)
844 844 prefixes.append(prefix)
845 845 parts.pop()
846 846
847 847 self.audited.add(path)
848 848 # only add prefixes to the cache after checking everything: we don't
849 849 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
850 850 self.auditeddir.update(prefixes)
851 851
852 852 def _makelock_file(info, pathname):
853 853 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
854 854 os.write(ld, info)
855 855 os.close(ld)
856 856
857 857 def _readlock_file(pathname):
858 858 return posixfile(pathname).read()
859 859
860 860 def nlinks(pathname):
861 861 """Return number of hardlinks for the given file."""
862 862 return os.lstat(pathname).st_nlink
863 863
864 864 if hasattr(os, 'link'):
865 865 os_link = os.link
866 866 else:
867 867 def os_link(src, dst):
868 868 raise OSError(0, _("Hardlinks not supported"))
869 869
870 870 def fstat(fp):
871 871 '''stat file object that may not have fileno method.'''
872 872 try:
873 873 return os.fstat(fp.fileno())
874 874 except AttributeError:
875 875 return os.stat(fp.name)
876 876
877 877 posixfile = file
878 878
879 879 def openhardlinks():
880 880 '''return true if it is safe to hold open file handles to hardlinks'''
881 881 return True
882 882
883 883 def _statfiles(files):
884 884 'Stat each file in files and yield stat or None if file does not exist.'
885 885 lstat = os.lstat
886 886 for nf in files:
887 887 try:
888 888 st = lstat(nf)
889 889 except OSError, err:
890 890 if err.errno not in (errno.ENOENT, errno.ENOTDIR):
891 891 raise
892 892 st = None
893 893 yield st
894 894
895 895 def _statfiles_clustered(files):
896 896 '''Stat each file in files and yield stat or None if file does not exist.
897 897 Cluster and cache stat per directory to minimize number of OS stat calls.'''
898 898 lstat = os.lstat
899 899 ncase = os.path.normcase
900 900 sep = os.sep
901 901 dircache = {} # dirname -> filename -> status | None if file does not exist
902 902 for nf in files:
903 903 nf = ncase(nf)
904 904 pos = nf.rfind(sep)
905 905 if pos == -1:
906 906 dir, base = '.', nf
907 907 else:
908 908 dir, base = nf[:pos+1], nf[pos+1:]
909 909 cache = dircache.get(dir, None)
910 910 if cache is None:
911 911 try:
912 912 dmap = dict([(ncase(n), s)
913 913 for n, k, s in osutil.listdir(dir, True)])
914 914 except OSError, err:
915 915 # handle directory not found in Python version prior to 2.5
916 916 # Python <= 2.4 returns native Windows code 3 in errno
917 917 # Python >= 2.5 returns ENOENT and adds winerror field
918 918 # EINVAL is raised if dir is not a directory.
919 919 if err.errno not in (3, errno.ENOENT, errno.EINVAL,
920 920 errno.ENOTDIR):
921 921 raise
922 922 dmap = {}
923 923 cache = dircache.setdefault(dir, dmap)
924 924 yield cache.get(base, None)
925 925
926 926 if sys.platform == 'win32':
927 927 statfiles = _statfiles_clustered
928 928 else:
929 929 statfiles = _statfiles
930 930
931 931 getuser_fallback = None
932 932
933 933 def getuser():
934 934 '''return name of current user'''
935 935 try:
936 936 return getpass.getuser()
937 937 except ImportError:
938 938 # import of pwd will fail on windows - try fallback
939 939 if getuser_fallback:
940 940 return getuser_fallback()
941 941 # raised if win32api not available
942 942 raise Abort(_('user name not available - set USERNAME '
943 943 'environment variable'))
944 944
945 945 def username(uid=None):
946 946 """Return the name of the user with the given uid.
947 947
948 948 If uid is None, return the name of the current user."""
949 949 try:
950 950 import pwd
951 951 if uid is None:
952 952 uid = os.getuid()
953 953 try:
954 954 return pwd.getpwuid(uid)[0]
955 955 except KeyError:
956 956 return str(uid)
957 957 except ImportError:
958 958 return None
959 959
960 960 def groupname(gid=None):
961 961 """Return the name of the group with the given gid.
962 962
963 963 If gid is None, return the name of the current group."""
964 964 try:
965 965 import grp
966 966 if gid is None:
967 967 gid = os.getgid()
968 968 try:
969 969 return grp.getgrgid(gid)[0]
970 970 except KeyError:
971 971 return str(gid)
972 972 except ImportError:
973 973 return None
974 974
975 975 # File system features
976 976
977 977 def checkcase(path):
978 978 """
979 979 Check whether the given path is on a case-sensitive filesystem
980 980
981 981 Requires a path (like /foo/.hg) ending with a foldable final
982 982 directory component.
983 983 """
984 984 s1 = os.stat(path)
985 985 d, b = os.path.split(path)
986 986 p2 = os.path.join(d, b.upper())
987 987 if path == p2:
988 988 p2 = os.path.join(d, b.lower())
989 989 try:
990 990 s2 = os.stat(p2)
991 991 if s2 == s1:
992 992 return False
993 993 return True
994 994 except:
995 995 return True
996 996
997 997 _fspathcache = {}
998 998 def fspath(name, root):
999 999 '''Get name in the case stored in the filesystem
1000 1000
1001 1001 The name is either relative to root, or it is an absolute path starting
1002 1002 with root. Note that this function is unnecessary, and should not be
1003 1003 called, for case-sensitive filesystems (simply because it's expensive).
1004 1004 '''
1005 1005 # If name is absolute, make it relative
1006 1006 if name.lower().startswith(root.lower()):
1007 1007 l = len(root)
1008 1008 if name[l] == os.sep or name[l] == os.altsep:
1009 1009 l = l + 1
1010 1010 name = name[l:]
1011 1011
1012 1012 if not os.path.exists(os.path.join(root, name)):
1013 1013 return None
1014 1014
1015 1015 seps = os.sep
1016 1016 if os.altsep:
1017 1017 seps = seps + os.altsep
1018 1018 # Protect backslashes. This gets silly very quickly.
1019 1019 seps.replace('\\','\\\\')
1020 1020 pattern = re.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
1021 1021 dir = os.path.normcase(os.path.normpath(root))
1022 1022 result = []
1023 1023 for part, sep in pattern.findall(name):
1024 1024 if sep:
1025 1025 result.append(sep)
1026 1026 continue
1027 1027
1028 1028 if dir not in _fspathcache:
1029 1029 _fspathcache[dir] = os.listdir(dir)
1030 1030 contents = _fspathcache[dir]
1031 1031
1032 1032 lpart = part.lower()
1033 1033 for n in contents:
1034 1034 if n.lower() == lpart:
1035 1035 result.append(n)
1036 1036 break
1037 1037 else:
1038 1038 # Cannot happen, as the file exists!
1039 1039 result.append(part)
1040 1040 dir = os.path.join(dir, lpart)
1041 1041
1042 1042 return ''.join(result)
1043 1043
1044 1044 def checkexec(path):
1045 1045 """
1046 1046 Check whether the given path is on a filesystem with UNIX-like exec flags
1047 1047
1048 1048 Requires a directory (like /foo/.hg)
1049 1049 """
1050 1050
1051 1051 # VFAT on some Linux versions can flip mode but it doesn't persist
1052 1052 # a FS remount. Frequently we can detect it if files are created
1053 1053 # with exec bit on.
1054 1054
1055 1055 try:
1056 1056 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
1057 1057 fh, fn = tempfile.mkstemp("", "", path)
1058 1058 try:
1059 1059 os.close(fh)
1060 1060 m = os.stat(fn).st_mode & 0777
1061 1061 new_file_has_exec = m & EXECFLAGS
1062 1062 os.chmod(fn, m ^ EXECFLAGS)
1063 1063 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0777) == m)
1064 1064 finally:
1065 1065 os.unlink(fn)
1066 1066 except (IOError, OSError):
1067 1067 # we don't care, the user probably won't be able to commit anyway
1068 1068 return False
1069 1069 return not (new_file_has_exec or exec_flags_cannot_flip)
1070 1070
1071 1071 def checklink(path):
1072 1072 """check whether the given path is on a symlink-capable filesystem"""
1073 1073 # mktemp is not racy because symlink creation will fail if the
1074 1074 # file already exists
1075 1075 name = tempfile.mktemp(dir=path)
1076 1076 try:
1077 1077 os.symlink(".", name)
1078 1078 os.unlink(name)
1079 1079 return True
1080 1080 except (OSError, AttributeError):
1081 1081 return False
1082 1082
1083 1083 _umask = os.umask(0)
1084 1084 os.umask(_umask)
1085 1085
1086 1086 def needbinarypatch():
1087 1087 """return True if patches should be applied in binary mode by default."""
1088 1088 return os.name == 'nt'
1089 1089
1090 1090 def endswithsep(path):
1091 1091 '''Check path ends with os.sep or os.altsep.'''
1092 1092 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
1093 1093
1094 1094 def splitpath(path):
1095 1095 '''Split path by os.sep.
1096 1096 Note that this function does not use os.altsep because this is
1097 1097 an alternative of simple "xxx.split(os.sep)".
1098 1098 It is recommended to use os.path.normpath() before using this
1099 1099 function if need.'''
1100 1100 return path.split(os.sep)
1101 1101
1102 1102 def gui():
1103 1103 '''Are we running in a GUI?'''
1104 1104 return os.name == "nt" or os.name == "mac" or os.environ.get("DISPLAY")
1105 1105
1106 1106 def lookup_reg(key, name=None, scope=None):
1107 1107 return None
1108 1108
1109 1109 # Platform specific variants
1110 1110 if os.name == 'nt':
1111 1111 import msvcrt
1112 1112 nulldev = 'NUL:'
1113 1113
1114 1114 class winstdout:
1115 1115 '''stdout on windows misbehaves if sent through a pipe'''
1116 1116
1117 1117 def __init__(self, fp):
1118 1118 self.fp = fp
1119 1119
1120 1120 def __getattr__(self, key):
1121 1121 return getattr(self.fp, key)
1122 1122
1123 1123 def close(self):
1124 1124 try:
1125 1125 self.fp.close()
1126 1126 except: pass
1127 1127
1128 1128 def write(self, s):
1129 1129 try:
1130 1130 # This is workaround for "Not enough space" error on
1131 1131 # writing large size of data to console.
1132 1132 limit = 16000
1133 1133 l = len(s)
1134 1134 start = 0
1135 1135 while start < l:
1136 1136 end = start + limit
1137 1137 self.fp.write(s[start:end])
1138 1138 start = end
1139 1139 except IOError, inst:
1140 1140 if inst.errno != 0: raise
1141 1141 self.close()
1142 1142 raise IOError(errno.EPIPE, 'Broken pipe')
1143 1143
1144 1144 def flush(self):
1145 1145 try:
1146 1146 return self.fp.flush()
1147 1147 except IOError, inst:
1148 1148 if inst.errno != errno.EINVAL: raise
1149 1149 self.close()
1150 1150 raise IOError(errno.EPIPE, 'Broken pipe')
1151 1151
1152 1152 sys.stdout = winstdout(sys.stdout)
1153 1153
1154 1154 def _is_win_9x():
1155 1155 '''return true if run on windows 95, 98 or me.'''
1156 1156 try:
1157 1157 return sys.getwindowsversion()[3] == 1
1158 1158 except AttributeError:
1159 1159 return 'command' in os.environ.get('comspec', '')
1160 1160
1161 1161 def openhardlinks():
1162 1162 return not _is_win_9x and "win32api" in locals()
1163 1163
1164 1164 def system_rcpath():
1165 1165 try:
1166 1166 return system_rcpath_win32()
1167 1167 except:
1168 1168 return [r'c:\mercurial\mercurial.ini']
1169 1169
1170 1170 def user_rcpath():
1171 1171 '''return os-specific hgrc search path to the user dir'''
1172 1172 try:
1173 1173 path = user_rcpath_win32()
1174 1174 except:
1175 1175 home = os.path.expanduser('~')
1176 1176 path = [os.path.join(home, 'mercurial.ini'),
1177 1177 os.path.join(home, '.hgrc')]
1178 1178 userprofile = os.environ.get('USERPROFILE')
1179 1179 if userprofile:
1180 1180 path.append(os.path.join(userprofile, 'mercurial.ini'))
1181 1181 path.append(os.path.join(userprofile, '.hgrc'))
1182 1182 return path
1183 1183
1184 1184 def parse_patch_output(output_line):
1185 1185 """parses the output produced by patch and returns the file name"""
1186 1186 pf = output_line[14:]
1187 1187 if pf[0] == '`':
1188 1188 pf = pf[1:-1] # Remove the quotes
1189 1189 return pf
1190 1190
1191 1191 def sshargs(sshcmd, host, user, port):
1192 1192 '''Build argument list for ssh or Plink'''
1193 1193 pflag = 'plink' in sshcmd.lower() and '-P' or '-p'
1194 1194 args = user and ("%s@%s" % (user, host)) or host
1195 1195 return port and ("%s %s %s" % (args, pflag, port)) or args
1196 1196
1197 1197 def testpid(pid):
1198 1198 '''return False if pid dead, True if running or not known'''
1199 1199 return True
1200 1200
1201 1201 def set_flags(f, l, x):
1202 1202 pass
1203 1203
1204 1204 def set_binary(fd):
1205 1205 # When run without console, pipes may expose invalid
1206 1206 # fileno(), usually set to -1.
1207 1207 if hasattr(fd, 'fileno') and fd.fileno() >= 0:
1208 1208 msvcrt.setmode(fd.fileno(), os.O_BINARY)
1209 1209
1210 1210 def pconvert(path):
1211 1211 return '/'.join(splitpath(path))
1212 1212
1213 1213 def localpath(path):
1214 1214 return path.replace('/', '\\')
1215 1215
1216 1216 def normpath(path):
1217 1217 return pconvert(os.path.normpath(path))
1218 1218
1219 1219 makelock = _makelock_file
1220 1220 readlock = _readlock_file
1221 1221
1222 1222 def samestat(s1, s2):
1223 1223 return False
1224 1224
1225 1225 # A sequence of backslashes is special iff it precedes a double quote:
1226 1226 # - if there's an even number of backslashes, the double quote is not
1227 1227 # quoted (i.e. it ends the quoted region)
1228 1228 # - if there's an odd number of backslashes, the double quote is quoted
1229 1229 # - in both cases, every pair of backslashes is unquoted into a single
1230 1230 # backslash
1231 1231 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
1232 1232 # So, to quote a string, we must surround it in double quotes, double
1233 1233 # the number of backslashes that preceed double quotes and add another
1234 1234 # backslash before every double quote (being careful with the double
1235 1235 # quote we've appended to the end)
1236 1236 _quotere = None
1237 1237 def shellquote(s):
1238 1238 global _quotere
1239 1239 if _quotere is None:
1240 1240 _quotere = re.compile(r'(\\*)("|\\$)')
1241 1241 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
1242 1242
1243 1243 def quotecommand(cmd):
1244 1244 """Build a command string suitable for os.popen* calls."""
1245 1245 # The extra quotes are needed because popen* runs the command
1246 1246 # through the current COMSPEC. cmd.exe suppress enclosing quotes.
1247 1247 return '"' + cmd + '"'
1248 1248
1249 1249 def popen(command, mode='r'):
1250 1250 # Work around "popen spawned process may not write to stdout
1251 1251 # under windows"
1252 1252 # http://bugs.python.org/issue1366
1253 1253 command += " 2> %s" % nulldev
1254 1254 return os.popen(quotecommand(command), mode)
1255 1255
1256 1256 def explain_exit(code):
1257 1257 return _("exited with status %d") % code, code
1258 1258
1259 1259 # if you change this stub into a real check, please try to implement the
1260 1260 # username and groupname functions above, too.
1261 1261 def isowner(fp, st=None):
1262 1262 return True
1263 1263
1264 1264 def find_in_path(name, path, default=None):
1265 1265 '''find name in search path. path can be string (will be split
1266 1266 with os.pathsep), or iterable thing that returns strings. if name
1267 1267 found, return path to name. else return default. name is looked up
1268 1268 using cmd.exe rules, using PATHEXT.'''
1269 1269 if isinstance(path, str):
1270 1270 path = path.split(os.pathsep)
1271 1271
1272 1272 pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
1273 1273 pathext = pathext.lower().split(os.pathsep)
1274 1274 isexec = os.path.splitext(name)[1].lower() in pathext
1275 1275
1276 1276 for p in path:
1277 1277 p_name = os.path.join(p, name)
1278 1278
1279 1279 if isexec and os.path.exists(p_name):
1280 1280 return p_name
1281 1281
1282 1282 for ext in pathext:
1283 1283 p_name_ext = p_name + ext
1284 1284 if os.path.exists(p_name_ext):
1285 1285 return p_name_ext
1286 1286 return default
1287 1287
1288 1288 def set_signal_handler():
1289 1289 try:
1290 1290 set_signal_handler_win32()
1291 1291 except NameError:
1292 1292 pass
1293 1293
1294 1294 try:
1295 1295 # override functions with win32 versions if possible
1296 1296 from util_win32 import *
1297 1297 if not _is_win_9x():
1298 1298 posixfile = posixfile_nt
1299 1299 except ImportError:
1300 1300 pass
1301 1301
1302 1302 else:
1303 1303 nulldev = '/dev/null'
1304 1304
1305 1305 def rcfiles(path):
1306 1306 rcs = [os.path.join(path, 'hgrc')]
1307 1307 rcdir = os.path.join(path, 'hgrc.d')
1308 1308 try:
1309 1309 rcs.extend([os.path.join(rcdir, f)
1310 1310 for f, kind in osutil.listdir(rcdir)
1311 1311 if f.endswith(".rc")])
1312 1312 except OSError:
1313 1313 pass
1314 1314 return rcs
1315 1315
1316 1316 def system_rcpath():
1317 1317 path = []
1318 1318 # old mod_python does not set sys.argv
1319 1319 if len(getattr(sys, 'argv', [])) > 0:
1320 1320 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
1321 1321 '/../etc/mercurial'))
1322 1322 path.extend(rcfiles('/etc/mercurial'))
1323 1323 return path
1324 1324
1325 1325 def user_rcpath():
1326 1326 return [os.path.expanduser('~/.hgrc')]
1327 1327
1328 1328 def parse_patch_output(output_line):
1329 1329 """parses the output produced by patch and returns the file name"""
1330 1330 pf = output_line[14:]
1331 1331 if os.sys.platform == 'OpenVMS':
1332 1332 if pf[0] == '`':
1333 1333 pf = pf[1:-1] # Remove the quotes
1334 1334 else:
1335 1335 if pf.startswith("'") and pf.endswith("'") and " " in pf:
1336 1336 pf = pf[1:-1] # Remove the quotes
1337 1337 return pf
1338 1338
1339 1339 def sshargs(sshcmd, host, user, port):
1340 1340 '''Build argument list for ssh'''
1341 1341 args = user and ("%s@%s" % (user, host)) or host
1342 1342 return port and ("%s -p %s" % (args, port)) or args
1343 1343
1344 1344 def is_exec(f):
1345 1345 """check whether a file is executable"""
1346 1346 return (os.lstat(f).st_mode & 0100 != 0)
1347 1347
1348 1348 def set_flags(f, l, x):
1349 1349 s = os.lstat(f).st_mode
1350 1350 if l:
1351 1351 if not stat.S_ISLNK(s):
1352 1352 # switch file to link
1353 1353 data = file(f).read()
1354 1354 os.unlink(f)
1355 1355 try:
1356 1356 os.symlink(data, f)
1357 1357 except:
1358 1358 # failed to make a link, rewrite file
1359 1359 file(f, "w").write(data)
1360 1360 # no chmod needed at this point
1361 1361 return
1362 1362 if stat.S_ISLNK(s):
1363 1363 # switch link to file
1364 1364 data = os.readlink(f)
1365 1365 os.unlink(f)
1366 1366 file(f, "w").write(data)
1367 1367 s = 0666 & ~_umask # avoid restatting for chmod
1368 1368
1369 1369 sx = s & 0100
1370 1370 if x and not sx:
1371 1371 # Turn on +x for every +r bit when making a file executable
1372 1372 # and obey umask.
1373 1373 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
1374 1374 elif not x and sx:
1375 1375 # Turn off all +x bits
1376 1376 os.chmod(f, s & 0666)
1377 1377
1378 1378 def set_binary(fd):
1379 1379 pass
1380 1380
1381 1381 def pconvert(path):
1382 1382 return path
1383 1383
1384 1384 def localpath(path):
1385 1385 return path
1386 1386
1387 1387 normpath = os.path.normpath
1388 1388 samestat = os.path.samestat
1389 1389
1390 1390 def makelock(info, pathname):
1391 1391 try:
1392 1392 os.symlink(info, pathname)
1393 1393 except OSError, why:
1394 1394 if why.errno == errno.EEXIST:
1395 1395 raise
1396 1396 else:
1397 1397 _makelock_file(info, pathname)
1398 1398
1399 1399 def readlock(pathname):
1400 1400 try:
1401 1401 return os.readlink(pathname)
1402 1402 except OSError, why:
1403 1403 if why.errno in (errno.EINVAL, errno.ENOSYS):
1404 1404 return _readlock_file(pathname)
1405 1405 else:
1406 1406 raise
1407 1407
1408 1408 def shellquote(s):
1409 1409 if os.sys.platform == 'OpenVMS':
1410 1410 return '"%s"' % s
1411 1411 else:
1412 1412 return "'%s'" % s.replace("'", "'\\''")
1413 1413
1414 1414 def quotecommand(cmd):
1415 1415 return cmd
1416 1416
1417 1417 def popen(command, mode='r'):
1418 1418 return os.popen(command, mode)
1419 1419
1420 1420 def testpid(pid):
1421 1421 '''return False if pid dead, True if running or not sure'''
1422 1422 if os.sys.platform == 'OpenVMS':
1423 1423 return True
1424 1424 try:
1425 1425 os.kill(pid, 0)
1426 1426 return True
1427 1427 except OSError, inst:
1428 1428 return inst.errno != errno.ESRCH
1429 1429
1430 1430 def explain_exit(code):
1431 1431 """return a 2-tuple (desc, code) describing a process's status"""
1432 1432 if os.WIFEXITED(code):
1433 1433 val = os.WEXITSTATUS(code)
1434 1434 return _("exited with status %d") % val, val
1435 1435 elif os.WIFSIGNALED(code):
1436 1436 val = os.WTERMSIG(code)
1437 1437 return _("killed by signal %d") % val, val
1438 1438 elif os.WIFSTOPPED(code):
1439 1439 val = os.WSTOPSIG(code)
1440 1440 return _("stopped by signal %d") % val, val
1441 1441 raise ValueError(_("invalid exit code"))
1442 1442
1443 1443 def isowner(fp, st=None):
1444 1444 """Return True if the file object f belongs to the current user.
1445 1445
1446 1446 The return value of a util.fstat(f) may be passed as the st argument.
1447 1447 """
1448 1448 if st is None:
1449 1449 st = fstat(fp)
1450 1450 return st.st_uid == os.getuid()
1451 1451
1452 1452 def find_in_path(name, path, default=None):
1453 1453 '''find name in search path. path can be string (will be split
1454 1454 with os.pathsep), or iterable thing that returns strings. if name
1455 1455 found, return path to name. else return default.'''
1456 1456 if isinstance(path, str):
1457 1457 path = path.split(os.pathsep)
1458 1458 for p in path:
1459 1459 p_name = os.path.join(p, name)
1460 1460 if os.path.exists(p_name):
1461 1461 return p_name
1462 1462 return default
1463 1463
1464 1464 def set_signal_handler():
1465 1465 pass
1466 1466
1467 1467 def find_exe(name, default=None):
1468 1468 '''find path of an executable.
1469 1469 if name contains a path component, return it as is. otherwise,
1470 1470 use normal executable search path.'''
1471 1471
1472 1472 if os.sep in name or sys.platform == 'OpenVMS':
1473 1473 # don't check the executable bit. if the file isn't
1474 1474 # executable, whoever tries to actually run it will give a
1475 1475 # much more useful error message.
1476 1476 return name
1477 1477 return find_in_path(name, os.environ.get('PATH', ''), default=default)
1478 1478
1479 1479 def mktempcopy(name, emptyok=False, createmode=None):
1480 1480 """Create a temporary file with the same contents from name
1481 1481
1482 1482 The permission bits are copied from the original file.
1483 1483
1484 1484 If the temporary file is going to be truncated immediately, you
1485 1485 can use emptyok=True as an optimization.
1486 1486
1487 1487 Returns the name of the temporary file.
1488 1488 """
1489 1489 d, fn = os.path.split(name)
1490 1490 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1491 1491 os.close(fd)
1492 1492 # Temporary files are created with mode 0600, which is usually not
1493 1493 # what we want. If the original file already exists, just copy
1494 1494 # its mode. Otherwise, manually obey umask.
1495 1495 try:
1496 1496 st_mode = os.lstat(name).st_mode & 0777
1497 1497 except OSError, inst:
1498 1498 if inst.errno != errno.ENOENT:
1499 1499 raise
1500 1500 st_mode = createmode
1501 1501 if st_mode is None:
1502 1502 st_mode = ~_umask
1503 1503 st_mode &= 0666
1504 1504 os.chmod(temp, st_mode)
1505 1505 if emptyok:
1506 1506 return temp
1507 1507 try:
1508 1508 try:
1509 1509 ifp = posixfile(name, "rb")
1510 1510 except IOError, inst:
1511 1511 if inst.errno == errno.ENOENT:
1512 1512 return temp
1513 1513 if not getattr(inst, 'filename', None):
1514 1514 inst.filename = name
1515 1515 raise
1516 1516 ofp = posixfile(temp, "wb")
1517 1517 for chunk in filechunkiter(ifp):
1518 1518 ofp.write(chunk)
1519 1519 ifp.close()
1520 1520 ofp.close()
1521 1521 except:
1522 1522 try: os.unlink(temp)
1523 1523 except: pass
1524 1524 raise
1525 1525 return temp
1526 1526
1527 1527 class atomictempfile(posixfile):
1528 1528 """file-like object that atomically updates a file
1529 1529
1530 1530 All writes will be redirected to a temporary copy of the original
1531 1531 file. When rename is called, the copy is renamed to the original
1532 1532 name, making the changes visible.
1533 1533 """
1534 1534 def __init__(self, name, mode, createmode):
1535 1535 self.__name = name
1536 1536 self.temp = mktempcopy(name, emptyok=('w' in mode),
1537 1537 createmode=createmode)
1538 1538 posixfile.__init__(self, self.temp, mode)
1539 1539
1540 1540 def rename(self):
1541 1541 if not self.closed:
1542 1542 posixfile.close(self)
1543 1543 rename(self.temp, localpath(self.__name))
1544 1544
1545 1545 def __del__(self):
1546 1546 if not self.closed:
1547 1547 try:
1548 1548 os.unlink(self.temp)
1549 1549 except: pass
1550 1550 posixfile.close(self)
1551 1551
1552 1552 def makedirs(name, mode=None):
1553 1553 """recursive directory creation with parent mode inheritance"""
1554 1554 try:
1555 1555 os.mkdir(name)
1556 1556 if mode is not None:
1557 1557 os.chmod(name, mode)
1558 1558 return
1559 1559 except OSError, err:
1560 1560 if err.errno == errno.EEXIST:
1561 1561 return
1562 1562 if err.errno != errno.ENOENT:
1563 1563 raise
1564 1564 parent = os.path.abspath(os.path.dirname(name))
1565 1565 makedirs(parent, mode)
1566 1566 makedirs(name, mode)
1567 1567
1568 1568 class opener(object):
1569 1569 """Open files relative to a base directory
1570 1570
1571 1571 This class is used to hide the details of COW semantics and
1572 1572 remote file access from higher level code.
1573 1573 """
1574 1574 def __init__(self, base, audit=True):
1575 1575 self.base = base
1576 1576 if audit:
1577 1577 self.audit_path = path_auditor(base)
1578 1578 else:
1579 1579 self.audit_path = always
1580 1580 self.createmode = None
1581 1581
1582 1582 def __getattr__(self, name):
1583 1583 if name == '_can_symlink':
1584 1584 self._can_symlink = checklink(self.base)
1585 1585 return self._can_symlink
1586 1586 raise AttributeError(name)
1587 1587
1588 1588 def _fixfilemode(self, name):
1589 1589 if self.createmode is None:
1590 1590 return
1591 1591 os.chmod(name, self.createmode & 0666)
1592 1592
1593 1593 def __call__(self, path, mode="r", text=False, atomictemp=False):
1594 1594 self.audit_path(path)
1595 1595 f = os.path.join(self.base, path)
1596 1596
1597 1597 if not text and "b" not in mode:
1598 1598 mode += "b" # for that other OS
1599 1599
1600 1600 nlink = -1
1601 1601 if mode not in ("r", "rb"):
1602 1602 try:
1603 1603 nlink = nlinks(f)
1604 1604 except OSError:
1605 1605 nlink = 0
1606 1606 d = os.path.dirname(f)
1607 1607 if not os.path.isdir(d):
1608 1608 makedirs(d, self.createmode)
1609 1609 if atomictemp:
1610 1610 return atomictempfile(f, mode, self.createmode)
1611 1611 if nlink > 1:
1612 1612 rename(mktempcopy(f), f)
1613 1613 fp = posixfile(f, mode)
1614 1614 if nlink == 0:
1615 1615 self._fixfilemode(f)
1616 1616 return fp
1617 1617
1618 1618 def symlink(self, src, dst):
1619 1619 self.audit_path(dst)
1620 1620 linkname = os.path.join(self.base, dst)
1621 1621 try:
1622 1622 os.unlink(linkname)
1623 1623 except OSError:
1624 1624 pass
1625 1625
1626 1626 dirname = os.path.dirname(linkname)
1627 1627 if not os.path.exists(dirname):
1628 1628 makedirs(dirname, self.createmode)
1629 1629
1630 1630 if self._can_symlink:
1631 1631 try:
1632 1632 os.symlink(src, linkname)
1633 1633 except OSError, err:
1634 1634 raise OSError(err.errno, _('could not symlink to %r: %s') %
1635 1635 (src, err.strerror), linkname)
1636 1636 else:
1637 1637 f = self(dst, "w")
1638 1638 f.write(src)
1639 1639 f.close()
1640 1640 self._fixfilemode(dst)
1641 1641
1642 1642 class chunkbuffer(object):
1643 1643 """Allow arbitrary sized chunks of data to be efficiently read from an
1644 1644 iterator over chunks of arbitrary size."""
1645 1645
1646 1646 def __init__(self, in_iter):
1647 1647 """in_iter is the iterator that's iterating over the input chunks.
1648 1648 targetsize is how big a buffer to try to maintain."""
1649 1649 self.iter = iter(in_iter)
1650 1650 self.buf = ''
1651 1651 self.targetsize = 2**16
1652 1652
1653 1653 def read(self, l):
1654 1654 """Read L bytes of data from the iterator of chunks of data.
1655 1655 Returns less than L bytes if the iterator runs dry."""
1656 1656 if l > len(self.buf) and self.iter:
1657 1657 # Clamp to a multiple of self.targetsize
1658 1658 targetsize = max(l, self.targetsize)
1659 1659 collector = cStringIO.StringIO()
1660 1660 collector.write(self.buf)
1661 1661 collected = len(self.buf)
1662 1662 for chunk in self.iter:
1663 1663 collector.write(chunk)
1664 1664 collected += len(chunk)
1665 1665 if collected >= targetsize:
1666 1666 break
1667 1667 if collected < targetsize:
1668 1668 self.iter = False
1669 1669 self.buf = collector.getvalue()
1670 1670 if len(self.buf) == l:
1671 1671 s, self.buf = str(self.buf), ''
1672 1672 else:
1673 1673 s, self.buf = self.buf[:l], buffer(self.buf, l)
1674 1674 return s
1675 1675
1676 1676 def filechunkiter(f, size=65536, limit=None):
1677 1677 """Create a generator that produces the data in the file size
1678 1678 (default 65536) bytes at a time, up to optional limit (default is
1679 1679 to read all data). Chunks may be less than size bytes if the
1680 1680 chunk is the last chunk in the file, or the file is a socket or
1681 1681 some other type of file that sometimes reads less data than is
1682 1682 requested."""
1683 1683 assert size >= 0
1684 1684 assert limit is None or limit >= 0
1685 1685 while True:
1686 1686 if limit is None: nbytes = size
1687 1687 else: nbytes = min(limit, size)
1688 1688 s = nbytes and f.read(nbytes)
1689 1689 if not s: break
1690 1690 if limit: limit -= len(s)
1691 1691 yield s
1692 1692
1693 1693 def makedate():
1694 1694 lt = time.localtime()
1695 1695 if lt[8] == 1 and time.daylight:
1696 1696 tz = time.altzone
1697 1697 else:
1698 1698 tz = time.timezone
1699 1699 return time.mktime(lt), tz
1700 1700
1701 1701 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1702 1702 """represent a (unixtime, offset) tuple as a localized time.
1703 1703 unixtime is seconds since the epoch, and offset is the time zone's
1704 1704 number of seconds away from UTC. if timezone is false, do not
1705 1705 append time zone to string."""
1706 1706 t, tz = date or makedate()
1707 1707 if "%1" in format or "%2" in format:
1708 1708 sign = (tz > 0) and "-" or "+"
1709 1709 minutes = abs(tz) / 60
1710 1710 format = format.replace("%1", "%c%02d" % (sign, minutes / 60))
1711 1711 format = format.replace("%2", "%02d" % (minutes % 60))
1712 1712 s = time.strftime(format, time.gmtime(float(t) - tz))
1713 1713 return s
1714 1714
1715 1715 def shortdate(date=None):
1716 1716 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1717 1717 return datestr(date, format='%Y-%m-%d')
1718 1718
1719 1719 def strdate(string, format, defaults=[]):
1720 1720 """parse a localized time string and return a (unixtime, offset) tuple.
1721 1721 if the string cannot be parsed, ValueError is raised."""
1722 1722 def timezone(string):
1723 1723 tz = string.split()[-1]
1724 1724 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1725 1725 sign = (tz[0] == "+") and 1 or -1
1726 1726 hours = int(tz[1:3])
1727 1727 minutes = int(tz[3:5])
1728 1728 return -sign * (hours * 60 + minutes) * 60
1729 1729 if tz == "GMT" or tz == "UTC":
1730 1730 return 0
1731 1731 return None
1732 1732
1733 1733 # NOTE: unixtime = localunixtime + offset
1734 1734 offset, date = timezone(string), string
1735 1735 if offset != None:
1736 1736 date = " ".join(string.split()[:-1])
1737 1737
1738 1738 # add missing elements from defaults
1739 1739 for part in defaults:
1740 1740 found = [True for p in part if ("%"+p) in format]
1741 1741 if not found:
1742 1742 date += "@" + defaults[part]
1743 1743 format += "@%" + part[0]
1744 1744
1745 1745 timetuple = time.strptime(date, format)
1746 1746 localunixtime = int(calendar.timegm(timetuple))
1747 1747 if offset is None:
1748 1748 # local timezone
1749 1749 unixtime = int(time.mktime(timetuple))
1750 1750 offset = unixtime - localunixtime
1751 1751 else:
1752 1752 unixtime = localunixtime + offset
1753 1753 return unixtime, offset
1754 1754
1755 1755 def parsedate(date, formats=None, defaults=None):
1756 1756 """parse a localized date/time string and return a (unixtime, offset) tuple.
1757 1757
1758 1758 The date may be a "unixtime offset" string or in one of the specified
1759 1759 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1760 1760 """
1761 1761 if not date:
1762 1762 return 0, 0
1763 1763 if isinstance(date, tuple) and len(date) == 2:
1764 1764 return date
1765 1765 if not formats:
1766 1766 formats = defaultdateformats
1767 1767 date = date.strip()
1768 1768 try:
1769 1769 when, offset = map(int, date.split(' '))
1770 1770 except ValueError:
1771 1771 # fill out defaults
1772 1772 if not defaults:
1773 1773 defaults = {}
1774 1774 now = makedate()
1775 1775 for part in "d mb yY HI M S".split():
1776 1776 if part not in defaults:
1777 1777 if part[0] in "HMS":
1778 1778 defaults[part] = "00"
1779 1779 else:
1780 1780 defaults[part] = datestr(now, "%" + part[0])
1781 1781
1782 1782 for format in formats:
1783 1783 try:
1784 1784 when, offset = strdate(date, format, defaults)
1785 1785 except (ValueError, OverflowError):
1786 1786 pass
1787 1787 else:
1788 1788 break
1789 1789 else:
1790 1790 raise Abort(_('invalid date: %r ') % date)
1791 1791 # validate explicit (probably user-specified) date and
1792 1792 # time zone offset. values must fit in signed 32 bits for
1793 1793 # current 32-bit linux runtimes. timezones go from UTC-12
1794 1794 # to UTC+14
1795 1795 if abs(when) > 0x7fffffff:
1796 1796 raise Abort(_('date exceeds 32 bits: %d') % when)
1797 1797 if offset < -50400 or offset > 43200:
1798 1798 raise Abort(_('impossible time zone offset: %d') % offset)
1799 1799 return when, offset
1800 1800
1801 1801 def matchdate(date):
1802 1802 """Return a function that matches a given date match specifier
1803 1803
1804 1804 Formats include:
1805 1805
1806 1806 '{date}' match a given date to the accuracy provided
1807 1807
1808 1808 '<{date}' on or before a given date
1809 1809
1810 1810 '>{date}' on or after a given date
1811 1811
1812 1812 """
1813 1813
1814 1814 def lower(date):
1815 1815 d = dict(mb="1", d="1")
1816 1816 return parsedate(date, extendeddateformats, d)[0]
1817 1817
1818 1818 def upper(date):
1819 1819 d = dict(mb="12", HI="23", M="59", S="59")
1820 1820 for days in "31 30 29".split():
1821 1821 try:
1822 1822 d["d"] = days
1823 1823 return parsedate(date, extendeddateformats, d)[0]
1824 1824 except:
1825 1825 pass
1826 1826 d["d"] = "28"
1827 1827 return parsedate(date, extendeddateformats, d)[0]
1828 1828
1829 1829 if date[0] == "<":
1830 1830 when = upper(date[1:])
1831 1831 return lambda x: x <= when
1832 1832 elif date[0] == ">":
1833 1833 when = lower(date[1:])
1834 1834 return lambda x: x >= when
1835 1835 elif date[0] == "-":
1836 1836 try:
1837 1837 days = int(date[1:])
1838 1838 except ValueError:
1839 1839 raise Abort(_("invalid day spec: %s") % date[1:])
1840 1840 when = makedate()[0] - days * 3600 * 24
1841 1841 return lambda x: x >= when
1842 1842 elif " to " in date:
1843 1843 a, b = date.split(" to ")
1844 1844 start, stop = lower(a), upper(b)
1845 1845 return lambda x: x >= start and x <= stop
1846 1846 else:
1847 1847 start, stop = lower(date), upper(date)
1848 1848 return lambda x: x >= start and x <= stop
1849 1849
1850 1850 def shortuser(user):
1851 1851 """Return a short representation of a user name or email address."""
1852 1852 f = user.find('@')
1853 1853 if f >= 0:
1854 1854 user = user[:f]
1855 1855 f = user.find('<')
1856 1856 if f >= 0:
1857 1857 user = user[f+1:]
1858 1858 f = user.find(' ')
1859 1859 if f >= 0:
1860 1860 user = user[:f]
1861 1861 f = user.find('.')
1862 1862 if f >= 0:
1863 1863 user = user[:f]
1864 1864 return user
1865 1865
1866 1866 def email(author):
1867 1867 '''get email of author.'''
1868 1868 r = author.find('>')
1869 1869 if r == -1: r = None
1870 1870 return author[author.find('<')+1:r]
1871 1871
1872 1872 def ellipsis(text, maxlength=400):
1873 1873 """Trim string to at most maxlength (default: 400) characters."""
1874 1874 if len(text) <= maxlength:
1875 1875 return text
1876 1876 else:
1877 1877 return "%s..." % (text[:maxlength-3])
1878 1878
1879 def walkrepos(path, followsym=False, seen_dirs=None):
1879 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
1880 1880 '''yield every hg repository under path, recursively.'''
1881 1881 def errhandler(err):
1882 1882 if err.filename == path:
1883 1883 raise err
1884 1884 if followsym and hasattr(os.path, 'samestat'):
1885 1885 def _add_dir_if_not_there(dirlst, dirname):
1886 1886 match = False
1887 1887 samestat = os.path.samestat
1888 1888 dirstat = os.stat(dirname)
1889 1889 for lstdirstat in dirlst:
1890 1890 if samestat(dirstat, lstdirstat):
1891 1891 match = True
1892 1892 break
1893 1893 if not match:
1894 1894 dirlst.append(dirstat)
1895 1895 return not match
1896 1896 else:
1897 1897 followsym = False
1898 1898
1899 1899 if (seen_dirs is None) and followsym:
1900 1900 seen_dirs = []
1901 1901 _add_dir_if_not_there(seen_dirs, path)
1902 1902 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
1903 1903 if '.hg' in dirs:
1904 dirs[:] = [] # don't descend further
1905 1904 yield root # found a repository
1906 qroot = os.path.join(root, '.hg', 'patches')
1907 if os.path.isdir(os.path.join(qroot, '.hg')):
1908 yield qroot # we have a patch queue repo here
1905 if recurse:
1906 # avoid recursing inside the .hg directory
1907 # the mq repository is added in any case
1908 dirs.remove('.hg')
1909 qroot = os.path.join(root, '.hg', 'patches')
1910 if os.path.isdir(os.path.join(qroot, '.hg')):
1911 yield qroot # we have a patch queue repo here
1912 else:
1913 dirs[:] = [] # don't descend further
1909 1914 elif followsym:
1910 1915 newdirs = []
1911 1916 for d in dirs:
1912 1917 fname = os.path.join(root, d)
1913 1918 if _add_dir_if_not_there(seen_dirs, fname):
1914 1919 if os.path.islink(fname):
1915 1920 for hgname in walkrepos(fname, True, seen_dirs):
1916 1921 yield hgname
1917 1922 else:
1918 1923 newdirs.append(d)
1919 1924 dirs[:] = newdirs
1920 1925
1921 1926 _rcpath = None
1922 1927
1923 1928 def os_rcpath():
1924 1929 '''return default os-specific hgrc search path'''
1925 1930 path = system_rcpath()
1926 1931 path.extend(user_rcpath())
1927 1932 path = [os.path.normpath(f) for f in path]
1928 1933 return path
1929 1934
1930 1935 def rcpath():
1931 1936 '''return hgrc search path. if env var HGRCPATH is set, use it.
1932 1937 for each item in path, if directory, use files ending in .rc,
1933 1938 else use item.
1934 1939 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1935 1940 if no HGRCPATH, use default os-specific path.'''
1936 1941 global _rcpath
1937 1942 if _rcpath is None:
1938 1943 if 'HGRCPATH' in os.environ:
1939 1944 _rcpath = []
1940 1945 for p in os.environ['HGRCPATH'].split(os.pathsep):
1941 1946 if not p: continue
1942 1947 if os.path.isdir(p):
1943 1948 for f, kind in osutil.listdir(p):
1944 1949 if f.endswith('.rc'):
1945 1950 _rcpath.append(os.path.join(p, f))
1946 1951 else:
1947 1952 _rcpath.append(p)
1948 1953 else:
1949 1954 _rcpath = os_rcpath()
1950 1955 return _rcpath
1951 1956
1952 1957 def bytecount(nbytes):
1953 1958 '''return byte count formatted as readable string, with units'''
1954 1959
1955 1960 units = (
1956 1961 (100, 1<<30, _('%.0f GB')),
1957 1962 (10, 1<<30, _('%.1f GB')),
1958 1963 (1, 1<<30, _('%.2f GB')),
1959 1964 (100, 1<<20, _('%.0f MB')),
1960 1965 (10, 1<<20, _('%.1f MB')),
1961 1966 (1, 1<<20, _('%.2f MB')),
1962 1967 (100, 1<<10, _('%.0f KB')),
1963 1968 (10, 1<<10, _('%.1f KB')),
1964 1969 (1, 1<<10, _('%.2f KB')),
1965 1970 (1, 1, _('%.0f bytes')),
1966 1971 )
1967 1972
1968 1973 for multiplier, divisor, format in units:
1969 1974 if nbytes >= divisor * multiplier:
1970 1975 return format % (nbytes / float(divisor))
1971 1976 return units[-1][2] % nbytes
1972 1977
1973 1978 def drop_scheme(scheme, path):
1974 1979 sc = scheme + ':'
1975 1980 if path.startswith(sc):
1976 1981 path = path[len(sc):]
1977 1982 if path.startswith('//'):
1978 1983 path = path[2:]
1979 1984 return path
1980 1985
1981 1986 def uirepr(s):
1982 1987 # Avoid double backslash in Windows path repr()
1983 1988 return repr(s).replace('\\\\', '\\')
@@ -1,88 +1,102 b''
1 1 #!/bin/sh
2 2 # Tests some basic hgwebdir functionality. Tests setting up paths and
3 3 # collection, different forms of 404s and the subdirectory support.
4 4
5 5 mkdir webdir
6 6 cd webdir
7 7
8 8 hg init a
9 9 echo a > a/a
10 10 hg --cwd a ci -Ama -d'1 0'
11 # create a mercurial queue repository
12 hg --cwd a qinit --config extensions.hgext.mq= -c
11 13
12 14 hg init b
13 15 echo b > b/b
14 16 hg --cwd b ci -Amb -d'2 0'
15 17
18 # create a nested repository
19 cd b
20 hg init d
21 echo d > d/d
22 hg --cwd d ci -Amd -d'3 0'
23 cd ..
24
16 25 hg init c
17 26 echo c > c/c
18 27 hg --cwd c ci -Amc -d'3 0'
28
19 29 root=`pwd`
20
21 30 cd ..
22 31
23 32 cat > paths.conf <<EOF
24 33 [paths]
25 34 a=$root/a
26 35 b=$root/b
27 36 EOF
28 37
29 38 hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf paths.conf \
30 39 -A access-paths.log -E error-paths-1.log
31 40 cat hg.pid >> $DAEMON_PIDS
32 41
33 42 echo % should give a 404 - file does not exist
34 43 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/bork?style=raw'
35 44
36 45 echo % should succeed
37 46 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/?style=raw'
38 47 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/a?style=raw'
39 48 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/b/file/tip/b?style=raw'
40 49
41 50 echo % should give a 404 - repo is not published
42 51 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/c/file/tip/c?style=raw'
43 52
44 53 cat > paths.conf <<EOF
45 54 [paths]
46 55 t/a/=$root/a
47 56 b=$root/b
48 57 coll=$root/*
58 rcoll=$root/**
49 59 EOF
50 60
51 61 hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf paths.conf \
52 62 -A access-paths.log -E error-paths-2.log
53 63 cat hg.pid >> $DAEMON_PIDS
54 64
55 65 echo % should succeed, slashy names
56 66 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=raw'
57 67 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t?style=raw'
58 68 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/?style=raw'
59 69 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a?style=atom' \
60 70 | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
61 71 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/?style=atom' \
62 72 | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
63 73 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/file/tip/a?style=raw'
64 74 # Test [paths] '*' extension
65 75 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/coll/?style=raw'
66 76 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/coll/a/file/tip/a?style=raw'
77 #test [paths] '**' extension
78 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/rcoll/?style=raw'
79 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/rcoll/b/d/file/tip/d?style=raw'
80
67 81
68 82 cat > collections.conf <<EOF
69 83 [collections]
70 84 $root=$root
71 85 EOF
72 86
73 87 hg serve -p $HGPORT2 -d --pid-file=hg.pid --webdir-conf collections.conf \
74 88 -A access-collections.log -E error-collections.log
75 89 cat hg.pid >> $DAEMON_PIDS
76 90
77 echo % should succeed
91 echo % collections: should succeed
78 92 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/?style=raw'
79 93 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/file/tip/a?style=raw'
80 94 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/b/file/tip/b?style=raw'
81 95 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/c/file/tip/c?style=raw'
82 96
83 97 echo % paths errors 1
84 98 cat error-paths-1.log
85 99 echo % paths errors 2
86 100 cat error-paths-2.log
87 101 echo % collections errors
88 102 cat error-collections.log
@@ -1,137 +1,155 b''
1 1 adding a
2 2 adding b
3 adding d
3 4 adding c
4 5 % should give a 404 - file does not exist
5 6 404 Not Found
6 7
7 8
8 9 error: bork@8580ff50825a: not found in manifest
9 10 % should succeed
10 11 200 Script output follows
11 12
12 13
13 14 /a/
14 15 /b/
15 16
16 17 200 Script output follows
17 18
18 19 a
19 20 200 Script output follows
20 21
21 22 b
22 23 % should give a 404 - repo is not published
23 24 404 Not Found
24 25
25 26
26 27 error: repository c not found
27 28 % should succeed, slashy names
28 29 200 Script output follows
29 30
30 31
31 32 /b/
32 33 /coll/a/
33 34 /coll/b/
34 35 /coll/c/
36 /rcoll/a/
37 /rcoll/a/.hg/patches/
38 /rcoll/b/
39 /rcoll/b/d/
40 /rcoll/c/
35 41 /t/a/
36 42
37 43 200 Script output follows
38 44
39 45
40 46 /t/a/
41 47
42 48 200 Script output follows
43 49
44 50
45 51 /t/a/
46 52
47 53 200 Script output follows
48 54
49 55 <?xml version="1.0" encoding="ascii"?>
50 56 <feed xmlns="http://127.0.0.1/2005/Atom">
51 57 <!-- Changelog -->
52 58 <id>http://127.0.0.1/t/a/</id>
53 59 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
54 60 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
55 61 <title>t/a Changelog</title>
56 62 <updated>1970-01-01T00:00:01+00:00</updated>
57 63
58 64 <entry>
59 65 <title>a</title>
60 66 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
61 67 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
62 68 <author>
63 69 <name>test</name>
64 70 <email>&#116;&#101;&#115;&#116;</email>
65 71 </author>
66 72 <updated>1970-01-01T00:00:01+00:00</updated>
67 73 <published>1970-01-01T00:00:01+00:00</published>
68 74 <content type="xhtml">
69 75 <div xmlns="http://127.0.0.1/1999/xhtml">
70 76 <pre xml:space="preserve">a</pre>
71 77 </div>
72 78 </content>
73 79 </entry>
74 80
75 81 </feed>
76 82 200 Script output follows
77 83
78 84 <?xml version="1.0" encoding="ascii"?>
79 85 <feed xmlns="http://127.0.0.1/2005/Atom">
80 86 <!-- Changelog -->
81 87 <id>http://127.0.0.1/t/a/</id>
82 88 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
83 89 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
84 90 <title>t/a Changelog</title>
85 91 <updated>1970-01-01T00:00:01+00:00</updated>
86 92
87 93 <entry>
88 94 <title>a</title>
89 95 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
90 96 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
91 97 <author>
92 98 <name>test</name>
93 99 <email>&#116;&#101;&#115;&#116;</email>
94 100 </author>
95 101 <updated>1970-01-01T00:00:01+00:00</updated>
96 102 <published>1970-01-01T00:00:01+00:00</published>
97 103 <content type="xhtml">
98 104 <div xmlns="http://127.0.0.1/1999/xhtml">
99 105 <pre xml:space="preserve">a</pre>
100 106 </div>
101 107 </content>
102 108 </entry>
103 109
104 110 </feed>
105 111 200 Script output follows
106 112
107 113 a
108 114 200 Script output follows
109 115
110 116
111 117 /coll/a/
112 118 /coll/b/
113 119 /coll/c/
114 120
115 121 200 Script output follows
116 122
117 123 a
118 % should succeed
124 200 Script output follows
125
126
127 /rcoll/a/
128 /rcoll/a/.hg/patches/
129 /rcoll/b/
130 /rcoll/b/d/
131 /rcoll/c/
132
133 200 Script output follows
134
135 d
136 % collections: should succeed
119 137 200 Script output follows
120 138
121 139
122 140 /a/
123 141 /b/
124 142 /c/
125 143
126 144 200 Script output follows
127 145
128 146 a
129 147 200 Script output follows
130 148
131 149 b
132 150 200 Script output follows
133 151
134 152 c
135 153 % paths errors 1
136 154 % paths errors 2
137 155 % collections errors
General Comments 0
You need to be logged in to leave comments. Login now