##// END OF EJS Templates
merge with stable
Matt Mackall -
r15002:b55c1c6a merge default
parent child Browse files
Show More
@@ -1,373 +1,376 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
10 10 from mercurial.i18n import _
11 11 from mercurial import ui, hg, scmutil, 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 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
37 37 repos.extend(urlrepos(prefix, roothead, paths))
38 38 return repos
39 39
40 40 def urlrepos(prefix, roothead, paths):
41 41 """yield url paths and filesystem paths from a list of repo paths
42 42
43 43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
44 44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
45 45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
46 46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
47 47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
48 48 """
49 49 for path in paths:
50 50 path = os.path.normpath(path)
51 51 yield (prefix + '/' +
52 52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
53 53
54 54 class hgwebdir(object):
55 55 refreshinterval = 20
56 56
57 57 def __init__(self, conf, baseui=None):
58 58 self.conf = conf
59 59 self.baseui = baseui
60 60 self.lastrefresh = 0
61 61 self.motd = None
62 62 self.refresh()
63 63
64 64 def refresh(self):
65 65 if self.lastrefresh + self.refreshinterval > time.time():
66 66 return
67 67
68 68 if self.baseui:
69 69 u = self.baseui.copy()
70 70 else:
71 71 u = ui.ui()
72 72 u.setconfig('ui', 'report_untrusted', 'off')
73 73 u.setconfig('ui', 'interactive', 'off')
74 74
75 75 if not isinstance(self.conf, (dict, list, tuple)):
76 76 map = {'paths': 'hgweb-paths'}
77 77 if not os.path.exists(self.conf):
78 78 raise util.Abort(_('config file %s not found!') % self.conf)
79 79 u.readconfig(self.conf, remap=map, trust=True)
80 80 paths = []
81 81 for name, ignored in u.configitems('hgweb-paths'):
82 82 for path in u.configlist('hgweb-paths', name):
83 83 paths.append((name, path))
84 84 elif isinstance(self.conf, (list, tuple)):
85 85 paths = self.conf
86 86 elif isinstance(self.conf, dict):
87 87 paths = self.conf.items()
88 88
89 89 repos = findrepos(paths)
90 90 for prefix, root in u.configitems('collections'):
91 91 prefix = util.pconvert(prefix)
92 92 for path in scmutil.walkrepos(root, followsym=True):
93 93 repo = os.path.normpath(path)
94 94 name = util.pconvert(repo)
95 95 if name.startswith(prefix):
96 96 name = name[len(prefix):]
97 97 repos.append((name.lstrip('/'), repo))
98 98
99 99 self.repos = repos
100 100 self.ui = u
101 101 encoding.encoding = self.ui.config('web', 'encoding',
102 102 encoding.encoding)
103 103 self.style = self.ui.config('web', 'style', 'paper')
104 104 self.templatepath = self.ui.config('web', 'templates', None)
105 105 self.stripecount = self.ui.config('web', 'stripes', 1)
106 106 if self.stripecount:
107 107 self.stripecount = int(self.stripecount)
108 108 self._baseurl = self.ui.config('web', 'baseurl')
109 109 self.lastrefresh = time.time()
110 110
111 111 def run(self):
112 112 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
113 113 raise RuntimeError("This function is only intended to be "
114 114 "called while running as a CGI script.")
115 115 import mercurial.hgweb.wsgicgi as wsgicgi
116 116 wsgicgi.launch(self)
117 117
118 118 def __call__(self, env, respond):
119 119 req = wsgirequest(env, respond)
120 120 return self.run_wsgi(req)
121 121
122 122 def read_allowed(self, ui, req):
123 123 """Check allow_read and deny_read config options of a repo's ui object
124 124 to determine user permissions. By default, with neither option set (or
125 125 both empty), allow all users to read the repo. There are two ways a
126 126 user can be denied read access: (1) deny_read is not empty, and the
127 127 user is unauthenticated or deny_read contains user (or *), and (2)
128 128 allow_read is not empty and the user is not in allow_read. Return True
129 129 if user is allowed to read the repo, else return False."""
130 130
131 131 user = req.env.get('REMOTE_USER')
132 132
133 133 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
134 134 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
135 135 return False
136 136
137 137 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
138 138 # by default, allow reading if no allow_read option has been set
139 139 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
140 140 return True
141 141
142 142 return False
143 143
144 144 def run_wsgi(self, req):
145 145 try:
146 146 try:
147 147 self.refresh()
148 148
149 149 virtual = req.env.get("PATH_INFO", "").strip('/')
150 150 tmpl = self.templater(req)
151 151 ctype = tmpl('mimetype', encoding=encoding.encoding)
152 152 ctype = templater.stringify(ctype)
153 153
154 154 # a static file
155 155 if virtual.startswith('static/') or 'static' in req.form:
156 156 if virtual.startswith('static/'):
157 157 fname = virtual[7:]
158 158 else:
159 159 fname = req.form['static'][0]
160 160 static = templater.templatepath('static')
161 161 return (staticfile(static, fname, req),)
162 162
163 163 # top-level index
164 164 elif not virtual:
165 165 req.respond(HTTP_OK, ctype)
166 166 return self.makeindex(req, tmpl)
167 167
168 168 # nested indexes and hgwebs
169 169
170 170 repos = dict(self.repos)
171 171 virtualrepo = virtual
172 172 while virtualrepo:
173 173 real = repos.get(virtualrepo)
174 174 if real:
175 175 req.env['REPO_NAME'] = virtualrepo
176 176 try:
177 177 repo = hg.repository(self.ui, real)
178 178 return hgweb(repo).run_wsgi(req)
179 179 except IOError, inst:
180 180 msg = inst.strerror
181 181 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
182 182 except error.RepoError, inst:
183 183 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
184 184
185 185 up = virtualrepo.rfind('/')
186 186 if up < 0:
187 187 break
188 188 virtualrepo = virtualrepo[:up]
189 189
190 190 # browse subdirectories
191 191 subdir = virtual + '/'
192 192 if [r for r in repos if r.startswith(subdir)]:
193 193 req.respond(HTTP_OK, ctype)
194 194 return self.makeindex(req, tmpl, subdir)
195 195
196 196 # prefixes not found
197 197 req.respond(HTTP_NOT_FOUND, ctype)
198 198 return tmpl("notfound", repo=virtual)
199 199
200 200 except ErrorResponse, err:
201 201 req.respond(err, ctype)
202 202 return tmpl('error', error=err.message or '')
203 203 finally:
204 204 tmpl = None
205 205
206 206 def makeindex(self, req, tmpl, subdir=""):
207 207
208 208 def archivelist(ui, nodeid, url):
209 209 allowed = ui.configlist("web", "allow_archive", untrusted=True)
210 210 archives = []
211 211 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
212 212 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
213 213 untrusted=True):
214 214 archives.append({"type" : i[0], "extension": i[1],
215 215 "node": nodeid, "url": url})
216 216 return archives
217 217
218 218 def rawentries(subdir="", **map):
219 219
220 220 descend = self.ui.configbool('web', 'descend', True)
221 221 for name, path in self.repos:
222 222
223 223 if not name.startswith(subdir):
224 224 continue
225 225 name = name[len(subdir):]
226 226 if not descend and '/' in name:
227 227 continue
228 228
229 229 u = self.ui.copy()
230 230 try:
231 231 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
232 232 except Exception, e:
233 233 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
234 234 continue
235 235 def get(section, name, default=None):
236 236 return u.config(section, name, default, untrusted=True)
237 237
238 238 if u.configbool("web", "hidden", untrusted=True):
239 239 continue
240 240
241 241 if not self.read_allowed(u, req):
242 242 continue
243 243
244 244 parts = [name]
245 245 if 'PATH_INFO' in req.env:
246 246 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
247 247 if req.env['SCRIPT_NAME']:
248 248 parts.insert(0, req.env['SCRIPT_NAME'])
249 249 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
250 250
251 251 # update time with local timezone
252 252 try:
253 253 r = hg.repository(self.ui, path)
254 254 except IOError:
255 255 u.warn(_('error accessing repository at %s\n') % path)
256 256 continue
257 257 except error.RepoError:
258 258 u.warn(_('error accessing repository at %s\n') % path)
259 259 continue
260 260 try:
261 261 d = (get_mtime(r.spath), util.makedate()[1])
262 262 except OSError:
263 263 continue
264 264
265 265 contact = get_contact(get)
266 266 description = get("web", "description", "")
267 267 name = get("web", "name", name)
268 268 row = dict(contact=contact or "unknown",
269 269 contact_sort=contact.upper() or "unknown",
270 270 name=name,
271 271 name_sort=name,
272 272 url=url,
273 273 description=description or "unknown",
274 274 description_sort=description.upper() or "unknown",
275 275 lastchange=d,
276 276 lastchange_sort=d[1]-d[0],
277 277 archives=archivelist(u, "tip", url))
278 278 yield row
279 279
280 280 sortdefault = None, False
281 281 def entries(sortcolumn="", descending=False, subdir="", **map):
282 282 rows = rawentries(subdir=subdir, **map)
283 283
284 284 if sortcolumn and sortdefault != (sortcolumn, descending):
285 285 sortkey = '%s_sort' % sortcolumn
286 286 rows = sorted(rows, key=lambda x: x[sortkey],
287 287 reverse=descending)
288 288 for row, parity in zip(rows, paritygen(self.stripecount)):
289 289 row['parity'] = parity
290 290 yield row
291 291
292 292 self.refresh()
293 293 sortable = ["name", "description", "contact", "lastchange"]
294 294 sortcolumn, descending = sortdefault
295 295 if 'sort' in req.form:
296 296 sortcolumn = req.form['sort'][0]
297 297 descending = sortcolumn.startswith('-')
298 298 if descending:
299 299 sortcolumn = sortcolumn[1:]
300 300 if sortcolumn not in sortable:
301 301 sortcolumn = ""
302 302
303 303 sort = [("sort_%s" % column,
304 304 "%s%s" % ((not descending and column == sortcolumn)
305 305 and "-" or "", column))
306 306 for column in sortable]
307 307
308 308 self.refresh()
309 309 self.updatereqenv(req.env)
310 310
311 311 return tmpl("index", entries=entries, subdir=subdir,
312 312 sortcolumn=sortcolumn, descending=descending,
313 313 **dict(sort))
314 314
315 315 def templater(self, req):
316 316
317 317 def header(**map):
318 318 yield tmpl('header', encoding=encoding.encoding, **map)
319 319
320 320 def footer(**map):
321 321 yield tmpl("footer", **map)
322 322
323 323 def motd(**map):
324 324 if self.motd is not None:
325 325 yield self.motd
326 326 else:
327 327 yield config('web', 'motd', '')
328 328
329 329 def config(section, name, default=None, untrusted=True):
330 330 return self.ui.config(section, name, default, untrusted)
331 331
332 332 self.updatereqenv(req.env)
333 333
334 334 url = req.env.get('SCRIPT_NAME', '')
335 335 if not url.endswith('/'):
336 336 url += '/'
337 337
338 338 vars = {}
339 339 styles = (
340 340 req.form.get('style', [None])[0],
341 341 config('web', 'style'),
342 342 'paper'
343 343 )
344 344 style, mapfile = templater.stylemap(styles, self.templatepath)
345 345 if style == styles[0]:
346 346 vars['style'] = style
347 347
348 348 start = url[-1] == '?' and '&' or '?'
349 349 sessionvars = webutil.sessionvars(vars, start)
350 350 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
351 351 logoimg = config('web', 'logoimg', 'hglogo.png')
352 352 staticurl = config('web', 'staticurl') or url + 'static/'
353 353 if not staticurl.endswith('/'):
354 354 staticurl += '/'
355 355
356 356 tmpl = templater.templater(mapfile,
357 357 defaults={"header": header,
358 358 "footer": footer,
359 359 "motd": motd,
360 360 "url": url,
361 361 "logourl": logourl,
362 362 "logoimg": logoimg,
363 363 "staticurl": staticurl,
364 364 "sessionvars": sessionvars})
365 365 return tmpl
366 366
367 367 def updatereqenv(self, env):
368 368 if self._baseurl is not None:
369 369 u = util.url(self._baseurl)
370 370 env['SERVER_NAME'] = u.host
371 371 if u.port:
372 372 env['SERVER_PORT'] = u.port
373 env['SCRIPT_NAME'] = '/' + u.path
373 path = u.path or ""
374 if not path.startswith('/'):
375 path = '/' + path
376 env['SCRIPT_NAME'] = path
@@ -1,714 +1,717 b''
1 1 # ui.py - user interface bits for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import errno, getpass, os, socket, sys, tempfile, traceback
10 10 import config, scmutil, util, error
11 11
12 12 class ui(object):
13 13 def __init__(self, src=None):
14 14 self._buffers = []
15 15 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
16 16 self._reportuntrusted = True
17 17 self._ocfg = config.config() # overlay
18 18 self._tcfg = config.config() # trusted
19 19 self._ucfg = config.config() # untrusted
20 20 self._trustusers = set()
21 21 self._trustgroups = set()
22 22
23 23 if src:
24 24 self.fout = src.fout
25 25 self.ferr = src.ferr
26 26 self.fin = src.fin
27 27
28 28 self._tcfg = src._tcfg.copy()
29 29 self._ucfg = src._ucfg.copy()
30 30 self._ocfg = src._ocfg.copy()
31 31 self._trustusers = src._trustusers.copy()
32 32 self._trustgroups = src._trustgroups.copy()
33 33 self.environ = src.environ
34 34 self.fixconfig()
35 35 else:
36 36 self.fout = sys.stdout
37 37 self.ferr = sys.stderr
38 38 self.fin = sys.stdin
39 39
40 40 # shared read-only environment
41 41 self.environ = os.environ
42 42 # we always trust global config files
43 43 for f in scmutil.rcpath():
44 44 self.readconfig(f, trust=True)
45 45
46 46 def copy(self):
47 47 return self.__class__(self)
48 48
49 49 def _trusted(self, fp, f):
50 50 st = util.fstat(fp)
51 51 if util.isowner(st):
52 52 return True
53 53
54 54 tusers, tgroups = self._trustusers, self._trustgroups
55 55 if '*' in tusers or '*' in tgroups:
56 56 return True
57 57
58 58 user = util.username(st.st_uid)
59 59 group = util.groupname(st.st_gid)
60 60 if user in tusers or group in tgroups or user == util.username():
61 61 return True
62 62
63 63 if self._reportuntrusted:
64 64 self.warn(_('Not trusting file %s from untrusted '
65 65 'user %s, group %s\n') % (f, user, group))
66 66 return False
67 67
68 68 def readconfig(self, filename, root=None, trust=False,
69 69 sections=None, remap=None):
70 70 try:
71 71 fp = open(filename)
72 72 except IOError:
73 73 if not sections: # ignore unless we were looking for something
74 74 return
75 75 raise
76 76
77 77 cfg = config.config()
78 78 trusted = sections or trust or self._trusted(fp, filename)
79 79
80 80 try:
81 81 cfg.read(filename, fp, sections=sections, remap=remap)
82 82 except error.ConfigError, inst:
83 83 if trusted:
84 84 raise
85 85 self.warn(_("Ignored: %s\n") % str(inst))
86 86
87 87 if self.plain():
88 88 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
89 89 'logtemplate', 'style',
90 90 'traceback', 'verbose'):
91 91 if k in cfg['ui']:
92 92 del cfg['ui'][k]
93 93 for k, v in cfg.items('defaults'):
94 94 del cfg['defaults'][k]
95 95 # Don't remove aliases from the configuration if in the exceptionlist
96 96 if self.plain('alias'):
97 97 for k, v in cfg.items('alias'):
98 98 del cfg['alias'][k]
99 99
100 100 if trusted:
101 101 self._tcfg.update(cfg)
102 102 self._tcfg.update(self._ocfg)
103 103 self._ucfg.update(cfg)
104 104 self._ucfg.update(self._ocfg)
105 105
106 106 if root is None:
107 107 root = os.path.expanduser('~')
108 108 self.fixconfig(root=root)
109 109
110 110 def fixconfig(self, root=None, section=None):
111 111 if section in (None, 'paths'):
112 112 # expand vars and ~
113 113 # translate paths relative to root (or home) into absolute paths
114 114 root = root or os.getcwd()
115 115 for c in self._tcfg, self._ucfg, self._ocfg:
116 116 for n, p in c.items('paths'):
117 117 if not p:
118 118 continue
119 119 if '%%' in p:
120 120 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
121 121 % (n, p, self.configsource('paths', n)))
122 122 p = p.replace('%%', '%')
123 123 p = util.expandpath(p)
124 124 if not util.hasscheme(p) and not os.path.isabs(p):
125 125 p = os.path.normpath(os.path.join(root, p))
126 126 c.set("paths", n, p)
127 127
128 128 if section in (None, 'ui'):
129 129 # update ui options
130 130 self.debugflag = self.configbool('ui', 'debug')
131 131 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
132 132 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
133 133 if self.verbose and self.quiet:
134 134 self.quiet = self.verbose = False
135 135 self._reportuntrusted = self.debugflag or self.configbool("ui",
136 136 "report_untrusted", True)
137 137 self.tracebackflag = self.configbool('ui', 'traceback', False)
138 138
139 139 if section in (None, 'trusted'):
140 140 # update trust information
141 141 self._trustusers.update(self.configlist('trusted', 'users'))
142 142 self._trustgroups.update(self.configlist('trusted', 'groups'))
143 143
144 144 def setconfig(self, section, name, value, overlay=True):
145 145 if overlay:
146 146 self._ocfg.set(section, name, value)
147 147 self._tcfg.set(section, name, value)
148 148 self._ucfg.set(section, name, value)
149 149 self.fixconfig(section=section)
150 150
151 151 def _data(self, untrusted):
152 152 return untrusted and self._ucfg or self._tcfg
153 153
154 154 def configsource(self, section, name, untrusted=False):
155 155 return self._data(untrusted).source(section, name) or 'none'
156 156
157 157 def config(self, section, name, default=None, untrusted=False):
158 158 value = self._data(untrusted).get(section, name, default)
159 159 if self.debugflag and not untrusted and self._reportuntrusted:
160 160 uvalue = self._ucfg.get(section, name)
161 161 if uvalue is not None and uvalue != value:
162 162 self.debug("ignoring untrusted configuration option "
163 163 "%s.%s = %s\n" % (section, name, uvalue))
164 164 return value
165 165
166 166 def configpath(self, section, name, default=None, untrusted=False):
167 167 'get a path config item, expanded relative to repo root or config file'
168 168 v = self.config(section, name, default, untrusted)
169 169 if v is None:
170 170 return None
171 171 if not os.path.isabs(v) or "://" not in v:
172 172 src = self.configsource(section, name, untrusted)
173 173 if ':' in src:
174 174 base = os.path.dirname(src.rsplit(':')[0])
175 175 v = os.path.join(base, os.path.expanduser(v))
176 176 return v
177 177
178 178 def configbool(self, section, name, default=False, untrusted=False):
179 179 """parse a configuration element as a boolean
180 180
181 181 >>> u = ui(); s = 'foo'
182 182 >>> u.setconfig(s, 'true', 'yes')
183 183 >>> u.configbool(s, 'true')
184 184 True
185 185 >>> u.setconfig(s, 'false', 'no')
186 186 >>> u.configbool(s, 'false')
187 187 False
188 188 >>> u.configbool(s, 'unknown')
189 189 False
190 190 >>> u.configbool(s, 'unknown', True)
191 191 True
192 192 >>> u.setconfig(s, 'invalid', 'somevalue')
193 193 >>> u.configbool(s, 'invalid')
194 194 Traceback (most recent call last):
195 195 ...
196 196 ConfigError: foo.invalid is not a boolean ('somevalue')
197 197 """
198 198
199 199 v = self.config(section, name, None, untrusted)
200 200 if v is None:
201 201 return default
202 202 if isinstance(v, bool):
203 203 return v
204 204 b = util.parsebool(v)
205 205 if b is None:
206 206 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
207 207 % (section, name, v))
208 208 return b
209 209
210 210 def configint(self, section, name, default=None, untrusted=False):
211 211 """parse a configuration element as an integer
212 212
213 213 >>> u = ui(); s = 'foo'
214 214 >>> u.setconfig(s, 'int1', '42')
215 215 >>> u.configint(s, 'int1')
216 216 42
217 217 >>> u.setconfig(s, 'int2', '-42')
218 218 >>> u.configint(s, 'int2')
219 219 -42
220 220 >>> u.configint(s, 'unknown', 7)
221 221 7
222 222 >>> u.setconfig(s, 'invalid', 'somevalue')
223 223 >>> u.configint(s, 'invalid')
224 224 Traceback (most recent call last):
225 225 ...
226 226 ConfigError: foo.invalid is not an integer ('somevalue')
227 227 """
228 228
229 229 v = self.config(section, name, None, untrusted)
230 230 if v is None:
231 231 return default
232 232 try:
233 233 return int(v)
234 234 except ValueError:
235 235 raise error.ConfigError(_("%s.%s is not an integer ('%s')")
236 236 % (section, name, v))
237 237
238 238 def configlist(self, section, name, default=None, untrusted=False):
239 239 """parse a configuration element as a list of comma/space separated
240 240 strings
241 241
242 242 >>> u = ui(); s = 'foo'
243 243 >>> u.setconfig(s, 'list1', 'this,is "a small" ,test')
244 244 >>> u.configlist(s, 'list1')
245 245 ['this', 'is', 'a small', 'test']
246 246 """
247 247
248 248 def _parse_plain(parts, s, offset):
249 249 whitespace = False
250 250 while offset < len(s) and (s[offset].isspace() or s[offset] == ','):
251 251 whitespace = True
252 252 offset += 1
253 253 if offset >= len(s):
254 254 return None, parts, offset
255 255 if whitespace:
256 256 parts.append('')
257 257 if s[offset] == '"' and not parts[-1]:
258 258 return _parse_quote, parts, offset + 1
259 259 elif s[offset] == '"' and parts[-1][-1] == '\\':
260 260 parts[-1] = parts[-1][:-1] + s[offset]
261 261 return _parse_plain, parts, offset + 1
262 262 parts[-1] += s[offset]
263 263 return _parse_plain, parts, offset + 1
264 264
265 265 def _parse_quote(parts, s, offset):
266 266 if offset < len(s) and s[offset] == '"': # ""
267 267 parts.append('')
268 268 offset += 1
269 269 while offset < len(s) and (s[offset].isspace() or
270 270 s[offset] == ','):
271 271 offset += 1
272 272 return _parse_plain, parts, offset
273 273
274 274 while offset < len(s) and s[offset] != '"':
275 275 if (s[offset] == '\\' and offset + 1 < len(s)
276 276 and s[offset + 1] == '"'):
277 277 offset += 1
278 278 parts[-1] += '"'
279 279 else:
280 280 parts[-1] += s[offset]
281 281 offset += 1
282 282
283 283 if offset >= len(s):
284 284 real_parts = _configlist(parts[-1])
285 285 if not real_parts:
286 286 parts[-1] = '"'
287 287 else:
288 288 real_parts[0] = '"' + real_parts[0]
289 289 parts = parts[:-1]
290 290 parts.extend(real_parts)
291 291 return None, parts, offset
292 292
293 293 offset += 1
294 294 while offset < len(s) and s[offset] in [' ', ',']:
295 295 offset += 1
296 296
297 297 if offset < len(s):
298 298 if offset + 1 == len(s) and s[offset] == '"':
299 299 parts[-1] += '"'
300 300 offset += 1
301 301 else:
302 302 parts.append('')
303 303 else:
304 304 return None, parts, offset
305 305
306 306 return _parse_plain, parts, offset
307 307
308 308 def _configlist(s):
309 309 s = s.rstrip(' ,')
310 310 if not s:
311 311 return []
312 312 parser, parts, offset = _parse_plain, [''], 0
313 313 while parser:
314 314 parser, parts, offset = parser(parts, s, offset)
315 315 return parts
316 316
317 317 result = self.config(section, name, untrusted=untrusted)
318 318 if result is None:
319 319 result = default or []
320 320 if isinstance(result, basestring):
321 321 result = _configlist(result.lstrip(' ,\n'))
322 322 if result is None:
323 323 result = default or []
324 324 return result
325 325
326 326 def has_section(self, section, untrusted=False):
327 327 '''tell whether section exists in config.'''
328 328 return section in self._data(untrusted)
329 329
330 330 def configitems(self, section, untrusted=False):
331 331 items = self._data(untrusted).items(section)
332 332 if self.debugflag and not untrusted and self._reportuntrusted:
333 333 for k, v in self._ucfg.items(section):
334 334 if self._tcfg.get(section, k) != v:
335 335 self.debug("ignoring untrusted configuration option "
336 336 "%s.%s = %s\n" % (section, k, v))
337 337 return items
338 338
339 339 def walkconfig(self, untrusted=False):
340 340 cfg = self._data(untrusted)
341 341 for section in cfg.sections():
342 342 for name, value in self.configitems(section, untrusted):
343 343 yield section, name, value
344 344
345 345 def plain(self, feature=None):
346 346 '''is plain mode active?
347 347
348 348 Plain mode means that all configuration variables which affect
349 349 the behavior and output of Mercurial should be
350 350 ignored. Additionally, the output should be stable,
351 351 reproducible and suitable for use in scripts or applications.
352 352
353 353 The only way to trigger plain mode is by setting either the
354 354 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
355 355
356 356 The return value can either be
357 357 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
358 358 - True otherwise
359 359 '''
360 360 if 'HGPLAIN' not in os.environ and 'HGPLAINEXCEPT' not in os.environ:
361 361 return False
362 362 exceptions = os.environ.get('HGPLAINEXCEPT', '').strip().split(',')
363 363 if feature and exceptions:
364 364 return feature not in exceptions
365 365 return True
366 366
367 367 def username(self):
368 368 """Return default username to be used in commits.
369 369
370 370 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
371 371 and stop searching if one of these is set.
372 372 If not found and ui.askusername is True, ask the user, else use
373 373 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
374 374 """
375 375 user = os.environ.get("HGUSER")
376 376 if user is None:
377 377 user = self.config("ui", "username")
378 378 if user is not None:
379 379 user = os.path.expandvars(user)
380 380 if user is None:
381 381 user = os.environ.get("EMAIL")
382 382 if user is None and self.configbool("ui", "askusername"):
383 383 user = self.prompt(_("enter a commit username:"), default=None)
384 384 if user is None and not self.interactive():
385 385 try:
386 386 user = '%s@%s' % (util.getuser(), socket.getfqdn())
387 387 self.warn(_("No username found, using '%s' instead\n") % user)
388 388 except KeyError:
389 389 pass
390 390 if not user:
391 391 raise util.Abort(_('no username supplied (see "hg help config")'))
392 392 if "\n" in user:
393 393 raise util.Abort(_("username %s contains a newline\n") % repr(user))
394 394 return user
395 395
396 396 def shortuser(self, user):
397 397 """Return a short representation of a user name or email address."""
398 398 if not self.verbose:
399 399 user = util.shortuser(user)
400 400 return user
401 401
402 402 def expandpath(self, loc, default=None):
403 403 """Return repository location relative to cwd or from [paths]"""
404 404 if util.hasscheme(loc) or os.path.isdir(os.path.join(loc, '.hg')):
405 405 return loc
406 406
407 407 path = self.config('paths', loc)
408 408 if not path and default is not None:
409 409 path = self.config('paths', default)
410 410 return path or loc
411 411
412 412 def pushbuffer(self):
413 413 self._buffers.append([])
414 414
415 415 def popbuffer(self, labeled=False):
416 416 '''pop the last buffer and return the buffered output
417 417
418 418 If labeled is True, any labels associated with buffered
419 419 output will be handled. By default, this has no effect
420 420 on the output returned, but extensions and GUI tools may
421 421 handle this argument and returned styled output. If output
422 422 is being buffered so it can be captured and parsed or
423 423 processed, labeled should not be set to True.
424 424 '''
425 425 return "".join(self._buffers.pop())
426 426
427 427 def write(self, *args, **opts):
428 428 '''write args to output
429 429
430 430 By default, this method simply writes to the buffer or stdout,
431 431 but extensions or GUI tools may override this method,
432 432 write_err(), popbuffer(), and label() to style output from
433 433 various parts of hg.
434 434
435 435 An optional keyword argument, "label", can be passed in.
436 436 This should be a string containing label names separated by
437 437 space. Label names take the form of "topic.type". For example,
438 438 ui.debug() issues a label of "ui.debug".
439 439
440 440 When labeling output for a specific command, a label of
441 441 "cmdname.type" is recommended. For example, status issues
442 442 a label of "status.modified" for modified files.
443 443 '''
444 444 if self._buffers:
445 445 self._buffers[-1].extend([str(a) for a in args])
446 446 else:
447 447 for a in args:
448 448 self.fout.write(str(a))
449 449
450 450 def write_err(self, *args, **opts):
451 451 try:
452 452 if not getattr(self.fout, 'closed', False):
453 453 self.fout.flush()
454 454 for a in args:
455 455 self.ferr.write(str(a))
456 456 # stderr may be buffered under win32 when redirected to files,
457 457 # including stdout.
458 458 if not getattr(self.ferr, 'closed', False):
459 459 self.ferr.flush()
460 460 except IOError, inst:
461 461 if inst.errno not in (errno.EPIPE, errno.EIO):
462 462 raise
463 463
464 464 def flush(self):
465 465 try: self.fout.flush()
466 466 except: pass
467 467 try: self.ferr.flush()
468 468 except: pass
469 469
470 470 def interactive(self):
471 471 '''is interactive input allowed?
472 472
473 473 An interactive session is a session where input can be reasonably read
474 474 from `sys.stdin'. If this function returns false, any attempt to read
475 475 from stdin should fail with an error, unless a sensible default has been
476 476 specified.
477 477
478 478 Interactiveness is triggered by the value of the `ui.interactive'
479 479 configuration variable or - if it is unset - when `sys.stdin' points
480 480 to a terminal device.
481 481
482 482 This function refers to input only; for output, see `ui.formatted()'.
483 483 '''
484 484 i = self.configbool("ui", "interactive", None)
485 485 if i is None:
486 486 # some environments replace stdin without implementing isatty
487 487 # usually those are non-interactive
488 488 return util.isatty(self.fin)
489 489
490 490 return i
491 491
492 492 def termwidth(self):
493 493 '''how wide is the terminal in columns?
494 494 '''
495 495 if 'COLUMNS' in os.environ:
496 496 try:
497 497 return int(os.environ['COLUMNS'])
498 498 except ValueError:
499 499 pass
500 500 return util.termwidth()
501 501
502 502 def formatted(self):
503 503 '''should formatted output be used?
504 504
505 505 It is often desirable to format the output to suite the output medium.
506 506 Examples of this are truncating long lines or colorizing messages.
507 507 However, this is not often not desirable when piping output into other
508 508 utilities, e.g. `grep'.
509 509
510 510 Formatted output is triggered by the value of the `ui.formatted'
511 511 configuration variable or - if it is unset - when `sys.stdout' points
512 512 to a terminal device. Please note that `ui.formatted' should be
513 513 considered an implementation detail; it is not intended for use outside
514 514 Mercurial or its extensions.
515 515
516 516 This function refers to output only; for input, see `ui.interactive()'.
517 517 This function always returns false when in plain mode, see `ui.plain()'.
518 518 '''
519 519 if self.plain():
520 520 return False
521 521
522 522 i = self.configbool("ui", "formatted", None)
523 523 if i is None:
524 524 # some environments replace stdout without implementing isatty
525 525 # usually those are non-interactive
526 526 return util.isatty(self.fout)
527 527
528 528 return i
529 529
530 530 def _readline(self, prompt=''):
531 531 if util.isatty(self.fin):
532 532 try:
533 533 # magically add command line editing support, where
534 534 # available
535 535 import readline
536 536 # force demandimport to really load the module
537 537 readline.read_history_file
538 538 # windows sometimes raises something other than ImportError
539 539 except Exception:
540 540 pass
541 541
542 # instead of trying to emulate raw_input, swap our in/out
543 # with sys.stdin/out
544 old = sys.stdout, sys.stdin
545 sys.stdout, sys.stdin = self.fout, self.fin
546 line = raw_input(prompt)
547 sys.stdout, sys.stdin = old
542 # call write() so output goes through subclassed implementation
543 # e.g. color extension on Windows
544 self.write(prompt)
545
546 # instead of trying to emulate raw_input, swap self.fin with sys.stdin
547 old = sys.stdin
548 sys.stdin = self.fin
549 line = raw_input()
550 sys.stdin = old
548 551
549 552 # When stdin is in binary mode on Windows, it can cause
550 553 # raw_input() to emit an extra trailing carriage return
551 554 if os.linesep == '\r\n' and line and line[-1] == '\r':
552 555 line = line[:-1]
553 556 return line
554 557
555 558 def prompt(self, msg, default="y"):
556 559 """Prompt user with msg, read response.
557 560 If ui is not interactive, the default is returned.
558 561 """
559 562 if not self.interactive():
560 563 self.write(msg, ' ', default, "\n")
561 564 return default
562 565 try:
563 566 r = self._readline(self.label(msg, 'ui.prompt') + ' ')
564 567 if not r:
565 568 return default
566 569 return r
567 570 except EOFError:
568 571 raise util.Abort(_('response expected'))
569 572
570 573 def promptchoice(self, msg, choices, default=0):
571 574 """Prompt user with msg, read response, and ensure it matches
572 575 one of the provided choices. The index of the choice is returned.
573 576 choices is a sequence of acceptable responses with the format:
574 577 ('&None', 'E&xec', 'Sym&link') Responses are case insensitive.
575 578 If ui is not interactive, the default is returned.
576 579 """
577 580 resps = [s[s.index('&')+1].lower() for s in choices]
578 581 while True:
579 582 r = self.prompt(msg, resps[default])
580 583 if r.lower() in resps:
581 584 return resps.index(r.lower())
582 585 self.write(_("unrecognized response\n"))
583 586
584 587 def getpass(self, prompt=None, default=None):
585 588 if not self.interactive():
586 589 return default
587 590 try:
588 591 return getpass.getpass(prompt or _('password: '))
589 592 except EOFError:
590 593 raise util.Abort(_('response expected'))
591 594 def status(self, *msg, **opts):
592 595 '''write status message to output (if ui.quiet is False)
593 596
594 597 This adds an output label of "ui.status".
595 598 '''
596 599 if not self.quiet:
597 600 opts['label'] = opts.get('label', '') + ' ui.status'
598 601 self.write(*msg, **opts)
599 602 def warn(self, *msg, **opts):
600 603 '''write warning message to output (stderr)
601 604
602 605 This adds an output label of "ui.warning".
603 606 '''
604 607 opts['label'] = opts.get('label', '') + ' ui.warning'
605 608 self.write_err(*msg, **opts)
606 609 def note(self, *msg, **opts):
607 610 '''write note to output (if ui.verbose is True)
608 611
609 612 This adds an output label of "ui.note".
610 613 '''
611 614 if self.verbose:
612 615 opts['label'] = opts.get('label', '') + ' ui.note'
613 616 self.write(*msg, **opts)
614 617 def debug(self, *msg, **opts):
615 618 '''write debug message to output (if ui.debugflag is True)
616 619
617 620 This adds an output label of "ui.debug".
618 621 '''
619 622 if self.debugflag:
620 623 opts['label'] = opts.get('label', '') + ' ui.debug'
621 624 self.write(*msg, **opts)
622 625 def edit(self, text, user):
623 626 (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt",
624 627 text=True)
625 628 try:
626 629 f = os.fdopen(fd, "w")
627 630 f.write(text)
628 631 f.close()
629 632
630 633 editor = self.geteditor()
631 634
632 635 util.system("%s \"%s\"" % (editor, name),
633 636 environ={'HGUSER': user},
634 637 onerr=util.Abort, errprefix=_("edit failed"),
635 638 out=self.fout)
636 639
637 640 f = open(name)
638 641 t = f.read()
639 642 f.close()
640 643 finally:
641 644 os.unlink(name)
642 645
643 646 return t
644 647
645 648 def traceback(self, exc=None):
646 649 '''print exception traceback if traceback printing enabled.
647 650 only to call in exception handler. returns true if traceback
648 651 printed.'''
649 652 if self.tracebackflag:
650 653 if exc:
651 654 traceback.print_exception(exc[0], exc[1], exc[2])
652 655 else:
653 656 traceback.print_exc()
654 657 return self.tracebackflag
655 658
656 659 def geteditor(self):
657 660 '''return editor to use'''
658 661 return (os.environ.get("HGEDITOR") or
659 662 self.config("ui", "editor") or
660 663 os.environ.get("VISUAL") or
661 664 os.environ.get("EDITOR", "vi"))
662 665
663 666 def progress(self, topic, pos, item="", unit="", total=None):
664 667 '''show a progress message
665 668
666 669 With stock hg, this is simply a debug message that is hidden
667 670 by default, but with extensions or GUI tools it may be
668 671 visible. 'topic' is the current operation, 'item' is a
669 672 non-numeric marker of the current position (ie the currently
670 673 in-process file), 'pos' is the current numeric position (ie
671 674 revision, bytes, etc.), unit is a corresponding unit label,
672 675 and total is the highest expected pos.
673 676
674 677 Multiple nested topics may be active at a time.
675 678
676 679 All topics should be marked closed by setting pos to None at
677 680 termination.
678 681 '''
679 682
680 683 if pos is None or not self.debugflag:
681 684 return
682 685
683 686 if unit:
684 687 unit = ' ' + unit
685 688 if item:
686 689 item = ' ' + item
687 690
688 691 if total:
689 692 pct = 100.0 * pos / total
690 693 self.debug('%s:%s %s/%s%s (%4.2f%%)\n'
691 694 % (topic, item, pos, total, unit, pct))
692 695 else:
693 696 self.debug('%s:%s %s%s\n' % (topic, item, pos, unit))
694 697
695 698 def log(self, service, message):
696 699 '''hook for logging facility extensions
697 700
698 701 service should be a readily-identifiable subsystem, which will
699 702 allow filtering.
700 703 message should be a newline-terminated string to log.
701 704 '''
702 705 pass
703 706
704 707 def label(self, msg, label):
705 708 '''style msg based on supplied label
706 709
707 710 Like ui.write(), this just returns msg unchanged, but extensions
708 711 and GUI tools can override it to allow styling output without
709 712 writing it.
710 713
711 714 ui.write(s, 'label') is equivalent to
712 715 ui.write(ui.label(s, 'label')).
713 716 '''
714 717 return msg
General Comments 0
You need to be logged in to leave comments. Login now