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