##// END OF EJS Templates
hgweb: use absolute_import
Yuya Nishihara -
r27046:37fcfe52 default
parent child Browse files
Show More
@@ -1,31 +1,37 b''
1 # hgweb/__init__.py - web interface to a mercurial repository
1 # hgweb/__init__.py - web interface to a mercurial repository
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 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005 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 from __future__ import absolute_import
10
9 import os
11 import os
10 import hgweb_mod, hgwebdir_mod
12
13 from . import (
14 hgweb_mod,
15 hgwebdir_mod,
16 )
11
17
12 def hgweb(config, name=None, baseui=None):
18 def hgweb(config, name=None, baseui=None):
13 '''create an hgweb wsgi object
19 '''create an hgweb wsgi object
14
20
15 config can be one of:
21 config can be one of:
16 - repo object (single repo view)
22 - repo object (single repo view)
17 - path to repo (single repo view)
23 - path to repo (single repo view)
18 - path to config file (multi-repo view)
24 - path to config file (multi-repo view)
19 - dict of virtual:real pairs (multi-repo view)
25 - dict of virtual:real pairs (multi-repo view)
20 - list of virtual:real tuples (multi-repo view)
26 - list of virtual:real tuples (multi-repo view)
21 '''
27 '''
22
28
23 if ((isinstance(config, str) and not os.path.isdir(config)) or
29 if ((isinstance(config, str) and not os.path.isdir(config)) or
24 isinstance(config, dict) or isinstance(config, list)):
30 isinstance(config, dict) or isinstance(config, list)):
25 # create a multi-dir interface
31 # create a multi-dir interface
26 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
32 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
27 return hgweb_mod.hgweb(config, name=name, baseui=baseui)
33 return hgweb_mod.hgweb(config, name=name, baseui=baseui)
28
34
29 def hgwebdir(config, baseui=None):
35 def hgwebdir(config, baseui=None):
30 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
36 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
31
37
@@ -1,189 +1,193 b''
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
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 from __future__ import absolute_import
10
9 import BaseHTTPServer
11 import BaseHTTPServer
10 import errno, mimetypes, os
12 import errno
13 import mimetypes
14 import os
11
15
12 HTTP_OK = 200
16 HTTP_OK = 200
13 HTTP_NOT_MODIFIED = 304
17 HTTP_NOT_MODIFIED = 304
14 HTTP_BAD_REQUEST = 400
18 HTTP_BAD_REQUEST = 400
15 HTTP_UNAUTHORIZED = 401
19 HTTP_UNAUTHORIZED = 401
16 HTTP_FORBIDDEN = 403
20 HTTP_FORBIDDEN = 403
17 HTTP_NOT_FOUND = 404
21 HTTP_NOT_FOUND = 404
18 HTTP_METHOD_NOT_ALLOWED = 405
22 HTTP_METHOD_NOT_ALLOWED = 405
19 HTTP_SERVER_ERROR = 500
23 HTTP_SERVER_ERROR = 500
20
24
21
25
22 def ismember(ui, username, userlist):
26 def ismember(ui, username, userlist):
23 """Check if username is a member of userlist.
27 """Check if username is a member of userlist.
24
28
25 If userlist has a single '*' member, all users are considered members.
29 If userlist has a single '*' member, all users are considered members.
26 Can be overridden by extensions to provide more complex authorization
30 Can be overridden by extensions to provide more complex authorization
27 schemes.
31 schemes.
28 """
32 """
29 return userlist == ['*'] or username in userlist
33 return userlist == ['*'] or username in userlist
30
34
31 def checkauthz(hgweb, req, op):
35 def checkauthz(hgweb, req, op):
32 '''Check permission for operation based on request data (including
36 '''Check permission for operation based on request data (including
33 authentication info). Return if op allowed, else raise an ErrorResponse
37 authentication info). Return if op allowed, else raise an ErrorResponse
34 exception.'''
38 exception.'''
35
39
36 user = req.env.get('REMOTE_USER')
40 user = req.env.get('REMOTE_USER')
37
41
38 deny_read = hgweb.configlist('web', 'deny_read')
42 deny_read = hgweb.configlist('web', 'deny_read')
39 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
43 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
40 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
44 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
41
45
42 allow_read = hgweb.configlist('web', 'allow_read')
46 allow_read = hgweb.configlist('web', 'allow_read')
43 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
47 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
44 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
48 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
45
49
46 if op == 'pull' and not hgweb.allowpull:
50 if op == 'pull' and not hgweb.allowpull:
47 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
51 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
48 elif op == 'pull' or op is None: # op is None for interface requests
52 elif op == 'pull' or op is None: # op is None for interface requests
49 return
53 return
50
54
51 # enforce that you can only push using POST requests
55 # enforce that you can only push using POST requests
52 if req.env['REQUEST_METHOD'] != 'POST':
56 if req.env['REQUEST_METHOD'] != 'POST':
53 msg = 'push requires POST request'
57 msg = 'push requires POST request'
54 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
58 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
55
59
56 # require ssl by default for pushing, auth info cannot be sniffed
60 # require ssl by default for pushing, auth info cannot be sniffed
57 # and replayed
61 # and replayed
58 scheme = req.env.get('wsgi.url_scheme')
62 scheme = req.env.get('wsgi.url_scheme')
59 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
63 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
60 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
64 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
61
65
62 deny = hgweb.configlist('web', 'deny_push')
66 deny = hgweb.configlist('web', 'deny_push')
63 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
67 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
64 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
68 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
65
69
66 allow = hgweb.configlist('web', 'allow_push')
70 allow = hgweb.configlist('web', 'allow_push')
67 if not (allow and ismember(hgweb.repo.ui, user, allow)):
71 if not (allow and ismember(hgweb.repo.ui, user, allow)):
68 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
72 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
69
73
70 # Hooks for hgweb permission checks; extensions can add hooks here.
74 # Hooks for hgweb permission checks; extensions can add hooks here.
71 # Each hook is invoked like this: hook(hgweb, request, operation),
75 # Each hook is invoked like this: hook(hgweb, request, operation),
72 # where operation is either read, pull or push. Hooks should either
76 # where operation is either read, pull or push. Hooks should either
73 # raise an ErrorResponse exception, or just return.
77 # raise an ErrorResponse exception, or just return.
74 #
78 #
75 # It is possible to do both authentication and authorization through
79 # It is possible to do both authentication and authorization through
76 # this.
80 # this.
77 permhooks = [checkauthz]
81 permhooks = [checkauthz]
78
82
79
83
80 class ErrorResponse(Exception):
84 class ErrorResponse(Exception):
81 def __init__(self, code, message=None, headers=[]):
85 def __init__(self, code, message=None, headers=[]):
82 if message is None:
86 if message is None:
83 message = _statusmessage(code)
87 message = _statusmessage(code)
84 Exception.__init__(self, message)
88 Exception.__init__(self, message)
85 self.code = code
89 self.code = code
86 self.headers = headers
90 self.headers = headers
87
91
88 class continuereader(object):
92 class continuereader(object):
89 def __init__(self, f, write):
93 def __init__(self, f, write):
90 self.f = f
94 self.f = f
91 self._write = write
95 self._write = write
92 self.continued = False
96 self.continued = False
93
97
94 def read(self, amt=-1):
98 def read(self, amt=-1):
95 if not self.continued:
99 if not self.continued:
96 self.continued = True
100 self.continued = True
97 self._write('HTTP/1.1 100 Continue\r\n\r\n')
101 self._write('HTTP/1.1 100 Continue\r\n\r\n')
98 return self.f.read(amt)
102 return self.f.read(amt)
99
103
100 def __getattr__(self, attr):
104 def __getattr__(self, attr):
101 if attr in ('close', 'readline', 'readlines', '__iter__'):
105 if attr in ('close', 'readline', 'readlines', '__iter__'):
102 return getattr(self.f, attr)
106 return getattr(self.f, attr)
103 raise AttributeError
107 raise AttributeError
104
108
105 def _statusmessage(code):
109 def _statusmessage(code):
106 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
110 responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
107 return responses.get(code, ('Error', 'Unknown error'))[0]
111 return responses.get(code, ('Error', 'Unknown error'))[0]
108
112
109 def statusmessage(code, message=None):
113 def statusmessage(code, message=None):
110 return '%d %s' % (code, message or _statusmessage(code))
114 return '%d %s' % (code, message or _statusmessage(code))
111
115
112 def get_stat(spath, fn):
116 def get_stat(spath, fn):
113 """stat fn if it exists, spath otherwise"""
117 """stat fn if it exists, spath otherwise"""
114 cl_path = os.path.join(spath, fn)
118 cl_path = os.path.join(spath, fn)
115 if os.path.exists(cl_path):
119 if os.path.exists(cl_path):
116 return os.stat(cl_path)
120 return os.stat(cl_path)
117 else:
121 else:
118 return os.stat(spath)
122 return os.stat(spath)
119
123
120 def get_mtime(spath):
124 def get_mtime(spath):
121 return get_stat(spath, "00changelog.i").st_mtime
125 return get_stat(spath, "00changelog.i").st_mtime
122
126
123 def staticfile(directory, fname, req):
127 def staticfile(directory, fname, req):
124 """return a file inside directory with guessed Content-Type header
128 """return a file inside directory with guessed Content-Type header
125
129
126 fname always uses '/' as directory separator and isn't allowed to
130 fname always uses '/' as directory separator and isn't allowed to
127 contain unusual path components.
131 contain unusual path components.
128 Content-Type is guessed using the mimetypes module.
132 Content-Type is guessed using the mimetypes module.
129 Return an empty string if fname is illegal or file not found.
133 Return an empty string if fname is illegal or file not found.
130
134
131 """
135 """
132 parts = fname.split('/')
136 parts = fname.split('/')
133 for part in parts:
137 for part in parts:
134 if (part in ('', os.curdir, os.pardir) or
138 if (part in ('', os.curdir, os.pardir) or
135 os.sep in part or os.altsep is not None and os.altsep in part):
139 os.sep in part or os.altsep is not None and os.altsep in part):
136 return
140 return
137 fpath = os.path.join(*parts)
141 fpath = os.path.join(*parts)
138 if isinstance(directory, str):
142 if isinstance(directory, str):
139 directory = [directory]
143 directory = [directory]
140 for d in directory:
144 for d in directory:
141 path = os.path.join(d, fpath)
145 path = os.path.join(d, fpath)
142 if os.path.exists(path):
146 if os.path.exists(path):
143 break
147 break
144 try:
148 try:
145 os.stat(path)
149 os.stat(path)
146 ct = mimetypes.guess_type(path)[0] or "text/plain"
150 ct = mimetypes.guess_type(path)[0] or "text/plain"
147 fp = open(path, 'rb')
151 fp = open(path, 'rb')
148 data = fp.read()
152 data = fp.read()
149 fp.close()
153 fp.close()
150 req.respond(HTTP_OK, ct, body=data)
154 req.respond(HTTP_OK, ct, body=data)
151 except TypeError:
155 except TypeError:
152 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
156 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
153 except OSError as err:
157 except OSError as err:
154 if err.errno == errno.ENOENT:
158 if err.errno == errno.ENOENT:
155 raise ErrorResponse(HTTP_NOT_FOUND)
159 raise ErrorResponse(HTTP_NOT_FOUND)
156 else:
160 else:
157 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
161 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
158
162
159 def paritygen(stripecount, offset=0):
163 def paritygen(stripecount, offset=0):
160 """count parity of horizontal stripes for easier reading"""
164 """count parity of horizontal stripes for easier reading"""
161 if stripecount and offset:
165 if stripecount and offset:
162 # account for offset, e.g. due to building the list in reverse
166 # account for offset, e.g. due to building the list in reverse
163 count = (stripecount + offset) % stripecount
167 count = (stripecount + offset) % stripecount
164 parity = (stripecount + offset) / stripecount & 1
168 parity = (stripecount + offset) / stripecount & 1
165 else:
169 else:
166 count = 0
170 count = 0
167 parity = 0
171 parity = 0
168 while True:
172 while True:
169 yield parity
173 yield parity
170 count += 1
174 count += 1
171 if stripecount and count >= stripecount:
175 if stripecount and count >= stripecount:
172 parity = 1 - parity
176 parity = 1 - parity
173 count = 0
177 count = 0
174
178
175 def get_contact(config):
179 def get_contact(config):
176 """Return repo contact information or empty string.
180 """Return repo contact information or empty string.
177
181
178 web.contact is the primary source, but if that is not set, try
182 web.contact is the primary source, but if that is not set, try
179 ui.username or $EMAIL as a fallback to display something useful.
183 ui.username or $EMAIL as a fallback to display something useful.
180 """
184 """
181 return (config("web", "contact") or
185 return (config("web", "contact") or
182 config("ui", "username") or
186 config("ui", "username") or
183 os.environ.get("EMAIL") or "")
187 os.environ.get("EMAIL") or "")
184
188
185 def caching(web, req):
189 def caching(web, req):
186 tag = str(web.mtime)
190 tag = str(web.mtime)
187 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
191 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
188 raise ErrorResponse(HTTP_NOT_MODIFIED)
192 raise ErrorResponse(HTTP_NOT_MODIFIED)
189 req.headers.append(('ETag', tag))
193 req.headers.append(('ETag', tag))
@@ -1,441 +1,466 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
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-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 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 from __future__ import absolute_import
10
9 import contextlib
11 import contextlib
10 import os
12 import os
11 from mercurial import hg, hook, error, encoding, templater, util, repoview
13
12 from mercurial import ui as uimod
14 from .common import (
13 from mercurial import templatefilters
15 ErrorResponse,
14 from common import ErrorResponse, permhooks, caching
16 HTTP_BAD_REQUEST,
15 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
17 HTTP_NOT_FOUND,
16 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
18 HTTP_NOT_MODIFIED,
17 from request import wsgirequest
19 HTTP_OK,
18 import webcommands, protocol, webutil, wsgicgi
20 HTTP_SERVER_ERROR,
21 caching,
22 permhooks,
23 )
24 from .request import wsgirequest
25
26 from .. import (
27 encoding,
28 error,
29 hg,
30 hook,
31 repoview,
32 templatefilters,
33 templater,
34 ui as uimod,
35 util,
36 )
37
38 from . import (
39 protocol,
40 webcommands,
41 webutil,
42 wsgicgi,
43 )
19
44
20 perms = {
45 perms = {
21 'changegroup': 'pull',
46 'changegroup': 'pull',
22 'changegroupsubset': 'pull',
47 'changegroupsubset': 'pull',
23 'getbundle': 'pull',
48 'getbundle': 'pull',
24 'stream_out': 'pull',
49 'stream_out': 'pull',
25 'listkeys': 'pull',
50 'listkeys': 'pull',
26 'unbundle': 'push',
51 'unbundle': 'push',
27 'pushkey': 'push',
52 'pushkey': 'push',
28 }
53 }
29
54
30 def makebreadcrumb(url, prefix=''):
55 def makebreadcrumb(url, prefix=''):
31 '''Return a 'URL breadcrumb' list
56 '''Return a 'URL breadcrumb' list
32
57
33 A 'URL breadcrumb' is a list of URL-name pairs,
58 A 'URL breadcrumb' is a list of URL-name pairs,
34 corresponding to each of the path items on a URL.
59 corresponding to each of the path items on a URL.
35 This can be used to create path navigation entries.
60 This can be used to create path navigation entries.
36 '''
61 '''
37 if url.endswith('/'):
62 if url.endswith('/'):
38 url = url[:-1]
63 url = url[:-1]
39 if prefix:
64 if prefix:
40 url = '/' + prefix + url
65 url = '/' + prefix + url
41 relpath = url
66 relpath = url
42 if relpath.startswith('/'):
67 if relpath.startswith('/'):
43 relpath = relpath[1:]
68 relpath = relpath[1:]
44
69
45 breadcrumb = []
70 breadcrumb = []
46 urlel = url
71 urlel = url
47 pathitems = [''] + relpath.split('/')
72 pathitems = [''] + relpath.split('/')
48 for pathel in reversed(pathitems):
73 for pathel in reversed(pathitems):
49 if not pathel or not urlel:
74 if not pathel or not urlel:
50 break
75 break
51 breadcrumb.append({'url': urlel, 'name': pathel})
76 breadcrumb.append({'url': urlel, 'name': pathel})
52 urlel = os.path.dirname(urlel)
77 urlel = os.path.dirname(urlel)
53 return reversed(breadcrumb)
78 return reversed(breadcrumb)
54
79
55 class requestcontext(object):
80 class requestcontext(object):
56 """Holds state/context for an individual request.
81 """Holds state/context for an individual request.
57
82
58 Servers can be multi-threaded. Holding state on the WSGI application
83 Servers can be multi-threaded. Holding state on the WSGI application
59 is prone to race conditions. Instances of this class exist to hold
84 is prone to race conditions. Instances of this class exist to hold
60 mutable and race-free state for requests.
85 mutable and race-free state for requests.
61 """
86 """
62 def __init__(self, app, repo):
87 def __init__(self, app, repo):
63 self.repo = repo
88 self.repo = repo
64 self.reponame = app.reponame
89 self.reponame = app.reponame
65
90
66 self.archives = ('zip', 'gz', 'bz2')
91 self.archives = ('zip', 'gz', 'bz2')
67
92
68 self.maxchanges = self.configint('web', 'maxchanges', 10)
93 self.maxchanges = self.configint('web', 'maxchanges', 10)
69 self.stripecount = self.configint('web', 'stripes', 1)
94 self.stripecount = self.configint('web', 'stripes', 1)
70 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
95 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
71 self.maxfiles = self.configint('web', 'maxfiles', 10)
96 self.maxfiles = self.configint('web', 'maxfiles', 10)
72 self.allowpull = self.configbool('web', 'allowpull', True)
97 self.allowpull = self.configbool('web', 'allowpull', True)
73
98
74 # we use untrusted=False to prevent a repo owner from using
99 # we use untrusted=False to prevent a repo owner from using
75 # web.templates in .hg/hgrc to get access to any file readable
100 # web.templates in .hg/hgrc to get access to any file readable
76 # by the user running the CGI script
101 # by the user running the CGI script
77 self.templatepath = self.config('web', 'templates', untrusted=False)
102 self.templatepath = self.config('web', 'templates', untrusted=False)
78
103
79 # This object is more expensive to build than simple config values.
104 # This object is more expensive to build than simple config values.
80 # It is shared across requests. The app will replace the object
105 # It is shared across requests. The app will replace the object
81 # if it is updated. Since this is a reference and nothing should
106 # if it is updated. Since this is a reference and nothing should
82 # modify the underlying object, it should be constant for the lifetime
107 # modify the underlying object, it should be constant for the lifetime
83 # of the request.
108 # of the request.
84 self.websubtable = app.websubtable
109 self.websubtable = app.websubtable
85
110
86 # Trust the settings from the .hg/hgrc files by default.
111 # Trust the settings from the .hg/hgrc files by default.
87 def config(self, section, name, default=None, untrusted=True):
112 def config(self, section, name, default=None, untrusted=True):
88 return self.repo.ui.config(section, name, default,
113 return self.repo.ui.config(section, name, default,
89 untrusted=untrusted)
114 untrusted=untrusted)
90
115
91 def configbool(self, section, name, default=False, untrusted=True):
116 def configbool(self, section, name, default=False, untrusted=True):
92 return self.repo.ui.configbool(section, name, default,
117 return self.repo.ui.configbool(section, name, default,
93 untrusted=untrusted)
118 untrusted=untrusted)
94
119
95 def configint(self, section, name, default=None, untrusted=True):
120 def configint(self, section, name, default=None, untrusted=True):
96 return self.repo.ui.configint(section, name, default,
121 return self.repo.ui.configint(section, name, default,
97 untrusted=untrusted)
122 untrusted=untrusted)
98
123
99 def configlist(self, section, name, default=None, untrusted=True):
124 def configlist(self, section, name, default=None, untrusted=True):
100 return self.repo.ui.configlist(section, name, default,
125 return self.repo.ui.configlist(section, name, default,
101 untrusted=untrusted)
126 untrusted=untrusted)
102
127
103 archivespecs = {
128 archivespecs = {
104 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
129 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
105 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
130 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
106 'zip': ('application/zip', 'zip', '.zip', None),
131 'zip': ('application/zip', 'zip', '.zip', None),
107 }
132 }
108
133
109 def archivelist(self, nodeid):
134 def archivelist(self, nodeid):
110 allowed = self.configlist('web', 'allow_archive')
135 allowed = self.configlist('web', 'allow_archive')
111 for typ, spec in self.archivespecs.iteritems():
136 for typ, spec in self.archivespecs.iteritems():
112 if typ in allowed or self.configbool('web', 'allow%s' % typ):
137 if typ in allowed or self.configbool('web', 'allow%s' % typ):
113 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
138 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
114
139
115 def templater(self, req):
140 def templater(self, req):
116 # determine scheme, port and server name
141 # determine scheme, port and server name
117 # this is needed to create absolute urls
142 # this is needed to create absolute urls
118
143
119 proto = req.env.get('wsgi.url_scheme')
144 proto = req.env.get('wsgi.url_scheme')
120 if proto == 'https':
145 if proto == 'https':
121 proto = 'https'
146 proto = 'https'
122 default_port = '443'
147 default_port = '443'
123 else:
148 else:
124 proto = 'http'
149 proto = 'http'
125 default_port = '80'
150 default_port = '80'
126
151
127 port = req.env['SERVER_PORT']
152 port = req.env['SERVER_PORT']
128 port = port != default_port and (':' + port) or ''
153 port = port != default_port and (':' + port) or ''
129 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
154 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
130 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
155 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
131 logoimg = self.config('web', 'logoimg', 'hglogo.png')
156 logoimg = self.config('web', 'logoimg', 'hglogo.png')
132 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
157 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
133 if not staticurl.endswith('/'):
158 if not staticurl.endswith('/'):
134 staticurl += '/'
159 staticurl += '/'
135
160
136 # some functions for the templater
161 # some functions for the templater
137
162
138 def motd(**map):
163 def motd(**map):
139 yield self.config('web', 'motd', '')
164 yield self.config('web', 'motd', '')
140
165
141 # figure out which style to use
166 # figure out which style to use
142
167
143 vars = {}
168 vars = {}
144 styles = (
169 styles = (
145 req.form.get('style', [None])[0],
170 req.form.get('style', [None])[0],
146 self.config('web', 'style'),
171 self.config('web', 'style'),
147 'paper',
172 'paper',
148 )
173 )
149 style, mapfile = templater.stylemap(styles, self.templatepath)
174 style, mapfile = templater.stylemap(styles, self.templatepath)
150 if style == styles[0]:
175 if style == styles[0]:
151 vars['style'] = style
176 vars['style'] = style
152
177
153 start = req.url[-1] == '?' and '&' or '?'
178 start = req.url[-1] == '?' and '&' or '?'
154 sessionvars = webutil.sessionvars(vars, start)
179 sessionvars = webutil.sessionvars(vars, start)
155
180
156 if not self.reponame:
181 if not self.reponame:
157 self.reponame = (self.config('web', 'name')
182 self.reponame = (self.config('web', 'name')
158 or req.env.get('REPO_NAME')
183 or req.env.get('REPO_NAME')
159 or req.url.strip('/') or self.repo.root)
184 or req.url.strip('/') or self.repo.root)
160
185
161 def websubfilter(text):
186 def websubfilter(text):
162 return templatefilters.websub(text, self.websubtable)
187 return templatefilters.websub(text, self.websubtable)
163
188
164 # create the templater
189 # create the templater
165
190
166 tmpl = templater.templater(mapfile,
191 tmpl = templater.templater(mapfile,
167 filters={'websub': websubfilter},
192 filters={'websub': websubfilter},
168 defaults={'url': req.url,
193 defaults={'url': req.url,
169 'logourl': logourl,
194 'logourl': logourl,
170 'logoimg': logoimg,
195 'logoimg': logoimg,
171 'staticurl': staticurl,
196 'staticurl': staticurl,
172 'urlbase': urlbase,
197 'urlbase': urlbase,
173 'repo': self.reponame,
198 'repo': self.reponame,
174 'encoding': encoding.encoding,
199 'encoding': encoding.encoding,
175 'motd': motd,
200 'motd': motd,
176 'sessionvars': sessionvars,
201 'sessionvars': sessionvars,
177 'pathdef': makebreadcrumb(req.url),
202 'pathdef': makebreadcrumb(req.url),
178 'style': style,
203 'style': style,
179 })
204 })
180 return tmpl
205 return tmpl
181
206
182
207
183 class hgweb(object):
208 class hgweb(object):
184 """HTTP server for individual repositories.
209 """HTTP server for individual repositories.
185
210
186 Instances of this class serve HTTP responses for a particular
211 Instances of this class serve HTTP responses for a particular
187 repository.
212 repository.
188
213
189 Instances are typically used as WSGI applications.
214 Instances are typically used as WSGI applications.
190
215
191 Some servers are multi-threaded. On these servers, there may
216 Some servers are multi-threaded. On these servers, there may
192 be multiple active threads inside __call__.
217 be multiple active threads inside __call__.
193 """
218 """
194 def __init__(self, repo, name=None, baseui=None):
219 def __init__(self, repo, name=None, baseui=None):
195 if isinstance(repo, str):
220 if isinstance(repo, str):
196 if baseui:
221 if baseui:
197 u = baseui.copy()
222 u = baseui.copy()
198 else:
223 else:
199 u = uimod.ui()
224 u = uimod.ui()
200 r = hg.repository(u, repo)
225 r = hg.repository(u, repo)
201 else:
226 else:
202 # we trust caller to give us a private copy
227 # we trust caller to give us a private copy
203 r = repo
228 r = repo
204
229
205 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
230 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
206 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
231 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
207 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
232 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
208 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
233 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
209 # resolve file patterns relative to repo root
234 # resolve file patterns relative to repo root
210 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
235 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
211 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
236 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
212 # displaying bundling progress bar while serving feel wrong and may
237 # displaying bundling progress bar while serving feel wrong and may
213 # break some wsgi implementation.
238 # break some wsgi implementation.
214 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
239 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
215 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
240 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
216 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
241 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
217 self._lastrepo = self._repos[0]
242 self._lastrepo = self._repos[0]
218 hook.redirect(True)
243 hook.redirect(True)
219 self.reponame = name
244 self.reponame = name
220
245
221 def _webifyrepo(self, repo):
246 def _webifyrepo(self, repo):
222 repo = getwebview(repo)
247 repo = getwebview(repo)
223 self.websubtable = webutil.getwebsubs(repo)
248 self.websubtable = webutil.getwebsubs(repo)
224 return repo
249 return repo
225
250
226 @contextlib.contextmanager
251 @contextlib.contextmanager
227 def _obtainrepo(self):
252 def _obtainrepo(self):
228 """Obtain a repo unique to the caller.
253 """Obtain a repo unique to the caller.
229
254
230 Internally we maintain a stack of cachedlocalrepo instances
255 Internally we maintain a stack of cachedlocalrepo instances
231 to be handed out. If one is available, we pop it and return it,
256 to be handed out. If one is available, we pop it and return it,
232 ensuring it is up to date in the process. If one is not available,
257 ensuring it is up to date in the process. If one is not available,
233 we clone the most recently used repo instance and return it.
258 we clone the most recently used repo instance and return it.
234
259
235 It is currently possible for the stack to grow without bounds
260 It is currently possible for the stack to grow without bounds
236 if the server allows infinite threads. However, servers should
261 if the server allows infinite threads. However, servers should
237 have a thread limit, thus establishing our limit.
262 have a thread limit, thus establishing our limit.
238 """
263 """
239 if self._repos:
264 if self._repos:
240 cached = self._repos.pop()
265 cached = self._repos.pop()
241 r, created = cached.fetch()
266 r, created = cached.fetch()
242 else:
267 else:
243 cached = self._lastrepo.copy()
268 cached = self._lastrepo.copy()
244 r, created = cached.fetch()
269 r, created = cached.fetch()
245 if created:
270 if created:
246 r = self._webifyrepo(r)
271 r = self._webifyrepo(r)
247
272
248 self._lastrepo = cached
273 self._lastrepo = cached
249 self.mtime = cached.mtime
274 self.mtime = cached.mtime
250 try:
275 try:
251 yield r
276 yield r
252 finally:
277 finally:
253 self._repos.append(cached)
278 self._repos.append(cached)
254
279
255 def run(self):
280 def run(self):
256 """Start a server from CGI environment.
281 """Start a server from CGI environment.
257
282
258 Modern servers should be using WSGI and should avoid this
283 Modern servers should be using WSGI and should avoid this
259 method, if possible.
284 method, if possible.
260 """
285 """
261 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
286 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
262 raise RuntimeError("This function is only intended to be "
287 raise RuntimeError("This function is only intended to be "
263 "called while running as a CGI script.")
288 "called while running as a CGI script.")
264 wsgicgi.launch(self)
289 wsgicgi.launch(self)
265
290
266 def __call__(self, env, respond):
291 def __call__(self, env, respond):
267 """Run the WSGI application.
292 """Run the WSGI application.
268
293
269 This may be called by multiple threads.
294 This may be called by multiple threads.
270 """
295 """
271 req = wsgirequest(env, respond)
296 req = wsgirequest(env, respond)
272 return self.run_wsgi(req)
297 return self.run_wsgi(req)
273
298
274 def run_wsgi(self, req):
299 def run_wsgi(self, req):
275 """Internal method to run the WSGI application.
300 """Internal method to run the WSGI application.
276
301
277 This is typically only called by Mercurial. External consumers
302 This is typically only called by Mercurial. External consumers
278 should be using instances of this class as the WSGI application.
303 should be using instances of this class as the WSGI application.
279 """
304 """
280 with self._obtainrepo() as repo:
305 with self._obtainrepo() as repo:
281 for r in self._runwsgi(req, repo):
306 for r in self._runwsgi(req, repo):
282 yield r
307 yield r
283
308
284 def _runwsgi(self, req, repo):
309 def _runwsgi(self, req, repo):
285 rctx = requestcontext(self, repo)
310 rctx = requestcontext(self, repo)
286
311
287 # This state is global across all threads.
312 # This state is global across all threads.
288 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
313 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
289 rctx.repo.ui.environ = req.env
314 rctx.repo.ui.environ = req.env
290
315
291 # work with CGI variables to create coherent structure
316 # work with CGI variables to create coherent structure
292 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
317 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
293
318
294 req.url = req.env['SCRIPT_NAME']
319 req.url = req.env['SCRIPT_NAME']
295 if not req.url.endswith('/'):
320 if not req.url.endswith('/'):
296 req.url += '/'
321 req.url += '/'
297 if 'REPO_NAME' in req.env:
322 if 'REPO_NAME' in req.env:
298 req.url += req.env['REPO_NAME'] + '/'
323 req.url += req.env['REPO_NAME'] + '/'
299
324
300 if 'PATH_INFO' in req.env:
325 if 'PATH_INFO' in req.env:
301 parts = req.env['PATH_INFO'].strip('/').split('/')
326 parts = req.env['PATH_INFO'].strip('/').split('/')
302 repo_parts = req.env.get('REPO_NAME', '').split('/')
327 repo_parts = req.env.get('REPO_NAME', '').split('/')
303 if parts[:len(repo_parts)] == repo_parts:
328 if parts[:len(repo_parts)] == repo_parts:
304 parts = parts[len(repo_parts):]
329 parts = parts[len(repo_parts):]
305 query = '/'.join(parts)
330 query = '/'.join(parts)
306 else:
331 else:
307 query = req.env['QUERY_STRING'].partition('&')[0]
332 query = req.env['QUERY_STRING'].partition('&')[0]
308 query = query.partition(';')[0]
333 query = query.partition(';')[0]
309
334
310 # process this if it's a protocol request
335 # process this if it's a protocol request
311 # protocol bits don't need to create any URLs
336 # protocol bits don't need to create any URLs
312 # and the clients always use the old URL structure
337 # and the clients always use the old URL structure
313
338
314 cmd = req.form.get('cmd', [''])[0]
339 cmd = req.form.get('cmd', [''])[0]
315 if protocol.iscmd(cmd):
340 if protocol.iscmd(cmd):
316 try:
341 try:
317 if query:
342 if query:
318 raise ErrorResponse(HTTP_NOT_FOUND)
343 raise ErrorResponse(HTTP_NOT_FOUND)
319 if cmd in perms:
344 if cmd in perms:
320 self.check_perm(rctx, req, perms[cmd])
345 self.check_perm(rctx, req, perms[cmd])
321 return protocol.call(rctx.repo, req, cmd)
346 return protocol.call(rctx.repo, req, cmd)
322 except ErrorResponse as inst:
347 except ErrorResponse as inst:
323 # A client that sends unbundle without 100-continue will
348 # A client that sends unbundle without 100-continue will
324 # break if we respond early.
349 # break if we respond early.
325 if (cmd == 'unbundle' and
350 if (cmd == 'unbundle' and
326 (req.env.get('HTTP_EXPECT',
351 (req.env.get('HTTP_EXPECT',
327 '').lower() != '100-continue') or
352 '').lower() != '100-continue') or
328 req.env.get('X-HgHttp2', '')):
353 req.env.get('X-HgHttp2', '')):
329 req.drain()
354 req.drain()
330 else:
355 else:
331 req.headers.append(('Connection', 'Close'))
356 req.headers.append(('Connection', 'Close'))
332 req.respond(inst, protocol.HGTYPE,
357 req.respond(inst, protocol.HGTYPE,
333 body='0\n%s\n' % inst)
358 body='0\n%s\n' % inst)
334 return ''
359 return ''
335
360
336 # translate user-visible url structure to internal structure
361 # translate user-visible url structure to internal structure
337
362
338 args = query.split('/', 2)
363 args = query.split('/', 2)
339 if 'cmd' not in req.form and args and args[0]:
364 if 'cmd' not in req.form and args and args[0]:
340
365
341 cmd = args.pop(0)
366 cmd = args.pop(0)
342 style = cmd.rfind('-')
367 style = cmd.rfind('-')
343 if style != -1:
368 if style != -1:
344 req.form['style'] = [cmd[:style]]
369 req.form['style'] = [cmd[:style]]
345 cmd = cmd[style + 1:]
370 cmd = cmd[style + 1:]
346
371
347 # avoid accepting e.g. style parameter as command
372 # avoid accepting e.g. style parameter as command
348 if util.safehasattr(webcommands, cmd):
373 if util.safehasattr(webcommands, cmd):
349 req.form['cmd'] = [cmd]
374 req.form['cmd'] = [cmd]
350
375
351 if cmd == 'static':
376 if cmd == 'static':
352 req.form['file'] = ['/'.join(args)]
377 req.form['file'] = ['/'.join(args)]
353 else:
378 else:
354 if args and args[0]:
379 if args and args[0]:
355 node = args.pop(0).replace('%2F', '/')
380 node = args.pop(0).replace('%2F', '/')
356 req.form['node'] = [node]
381 req.form['node'] = [node]
357 if args:
382 if args:
358 req.form['file'] = args
383 req.form['file'] = args
359
384
360 ua = req.env.get('HTTP_USER_AGENT', '')
385 ua = req.env.get('HTTP_USER_AGENT', '')
361 if cmd == 'rev' and 'mercurial' in ua:
386 if cmd == 'rev' and 'mercurial' in ua:
362 req.form['style'] = ['raw']
387 req.form['style'] = ['raw']
363
388
364 if cmd == 'archive':
389 if cmd == 'archive':
365 fn = req.form['node'][0]
390 fn = req.form['node'][0]
366 for type_, spec in rctx.archivespecs.iteritems():
391 for type_, spec in rctx.archivespecs.iteritems():
367 ext = spec[2]
392 ext = spec[2]
368 if fn.endswith(ext):
393 if fn.endswith(ext):
369 req.form['node'] = [fn[:-len(ext)]]
394 req.form['node'] = [fn[:-len(ext)]]
370 req.form['type'] = [type_]
395 req.form['type'] = [type_]
371
396
372 # process the web interface request
397 # process the web interface request
373
398
374 try:
399 try:
375 tmpl = rctx.templater(req)
400 tmpl = rctx.templater(req)
376 ctype = tmpl('mimetype', encoding=encoding.encoding)
401 ctype = tmpl('mimetype', encoding=encoding.encoding)
377 ctype = templater.stringify(ctype)
402 ctype = templater.stringify(ctype)
378
403
379 # check read permissions non-static content
404 # check read permissions non-static content
380 if cmd != 'static':
405 if cmd != 'static':
381 self.check_perm(rctx, req, None)
406 self.check_perm(rctx, req, None)
382
407
383 if cmd == '':
408 if cmd == '':
384 req.form['cmd'] = [tmpl.cache['default']]
409 req.form['cmd'] = [tmpl.cache['default']]
385 cmd = req.form['cmd'][0]
410 cmd = req.form['cmd'][0]
386
411
387 if rctx.configbool('web', 'cache', True):
412 if rctx.configbool('web', 'cache', True):
388 caching(self, req) # sets ETag header or raises NOT_MODIFIED
413 caching(self, req) # sets ETag header or raises NOT_MODIFIED
389 if cmd not in webcommands.__all__:
414 if cmd not in webcommands.__all__:
390 msg = 'no such method: %s' % cmd
415 msg = 'no such method: %s' % cmd
391 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
416 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
392 elif cmd == 'file' and 'raw' in req.form.get('style', []):
417 elif cmd == 'file' and 'raw' in req.form.get('style', []):
393 rctx.ctype = ctype
418 rctx.ctype = ctype
394 content = webcommands.rawfile(rctx, req, tmpl)
419 content = webcommands.rawfile(rctx, req, tmpl)
395 else:
420 else:
396 content = getattr(webcommands, cmd)(rctx, req, tmpl)
421 content = getattr(webcommands, cmd)(rctx, req, tmpl)
397 req.respond(HTTP_OK, ctype)
422 req.respond(HTTP_OK, ctype)
398
423
399 return content
424 return content
400
425
401 except (error.LookupError, error.RepoLookupError) as err:
426 except (error.LookupError, error.RepoLookupError) as err:
402 req.respond(HTTP_NOT_FOUND, ctype)
427 req.respond(HTTP_NOT_FOUND, ctype)
403 msg = str(err)
428 msg = str(err)
404 if (util.safehasattr(err, 'name') and
429 if (util.safehasattr(err, 'name') and
405 not isinstance(err, error.ManifestLookupError)):
430 not isinstance(err, error.ManifestLookupError)):
406 msg = 'revision not found: %s' % err.name
431 msg = 'revision not found: %s' % err.name
407 return tmpl('error', error=msg)
432 return tmpl('error', error=msg)
408 except (error.RepoError, error.RevlogError) as inst:
433 except (error.RepoError, error.RevlogError) as inst:
409 req.respond(HTTP_SERVER_ERROR, ctype)
434 req.respond(HTTP_SERVER_ERROR, ctype)
410 return tmpl('error', error=str(inst))
435 return tmpl('error', error=str(inst))
411 except ErrorResponse as inst:
436 except ErrorResponse as inst:
412 req.respond(inst, ctype)
437 req.respond(inst, ctype)
413 if inst.code == HTTP_NOT_MODIFIED:
438 if inst.code == HTTP_NOT_MODIFIED:
414 # Not allowed to return a body on a 304
439 # Not allowed to return a body on a 304
415 return ['']
440 return ['']
416 return tmpl('error', error=str(inst))
441 return tmpl('error', error=str(inst))
417
442
418 def check_perm(self, rctx, req, op):
443 def check_perm(self, rctx, req, op):
419 for permhook in permhooks:
444 for permhook in permhooks:
420 permhook(rctx, req, op)
445 permhook(rctx, req, op)
421
446
422 def getwebview(repo):
447 def getwebview(repo):
423 """The 'web.view' config controls changeset filter to hgweb. Possible
448 """The 'web.view' config controls changeset filter to hgweb. Possible
424 values are ``served``, ``visible`` and ``all``. Default is ``served``.
449 values are ``served``, ``visible`` and ``all``. Default is ``served``.
425 The ``served`` filter only shows changesets that can be pulled from the
450 The ``served`` filter only shows changesets that can be pulled from the
426 hgweb instance. The``visible`` filter includes secret changesets but
451 hgweb instance. The``visible`` filter includes secret changesets but
427 still excludes "hidden" one.
452 still excludes "hidden" one.
428
453
429 See the repoview module for details.
454 See the repoview module for details.
430
455
431 The option has been around undocumented since Mercurial 2.5, but no
456 The option has been around undocumented since Mercurial 2.5, but no
432 user ever asked about it. So we better keep it undocumented for now."""
457 user ever asked about it. So we better keep it undocumented for now."""
433 viewconfig = repo.ui.config('web', 'view', 'served',
458 viewconfig = repo.ui.config('web', 'view', 'served',
434 untrusted=True)
459 untrusted=True)
435 if viewconfig == 'all':
460 if viewconfig == 'all':
436 return repo.unfiltered()
461 return repo.unfiltered()
437 elif viewconfig in repoview.filtertable:
462 elif viewconfig in repoview.filtertable:
438 return repo.filtered(viewconfig)
463 return repo.filtered(viewconfig)
439 else:
464 else:
440 return repo.filtered('served')
465 return repo.filtered('served')
441
466
@@ -1,485 +1,511 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 from __future__ import absolute_import
10 from mercurial.i18n import _
10
11 from mercurial import hg, scmutil, util, templater
11 import os
12 from mercurial import ui as uimod
12 import re
13 from mercurial import error, encoding
13 import time
14 from common import ErrorResponse, get_mtime, staticfile, paritygen, ismember, \
14
15 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from ..i18n import _
16 import hgweb_mod
16
17 from request import wsgirequest
17 from .common import (
18 import webutil, wsgicgi
18 ErrorResponse,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
22 get_contact,
23 get_mtime,
24 ismember,
25 paritygen,
26 staticfile,
27 )
28 from .request import wsgirequest
29
30 from .. import (
31 encoding,
32 error,
33 hg,
34 scmutil,
35 templater,
36 ui as uimod,
37 util,
38 )
39
40 from . import (
41 hgweb_mod,
42 webutil,
43 wsgicgi,
44 )
19
45
20 def cleannames(items):
46 def cleannames(items):
21 return [(util.pconvert(name).strip('/'), path) for name, path in items]
47 return [(util.pconvert(name).strip('/'), path) for name, path in items]
22
48
23 def findrepos(paths):
49 def findrepos(paths):
24 repos = []
50 repos = []
25 for prefix, root in cleannames(paths):
51 for prefix, root in cleannames(paths):
26 roothead, roottail = os.path.split(root)
52 roothead, roottail = os.path.split(root)
27 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
53 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
28 # /bar/ be served as as foo/N .
54 # /bar/ be served as as foo/N .
29 # '*' will not search inside dirs with .hg (except .hg/patches),
55 # '*' will not search inside dirs with .hg (except .hg/patches),
30 # '**' will search inside dirs with .hg (and thus also find subrepos).
56 # '**' will search inside dirs with .hg (and thus also find subrepos).
31 try:
57 try:
32 recurse = {'*': False, '**': True}[roottail]
58 recurse = {'*': False, '**': True}[roottail]
33 except KeyError:
59 except KeyError:
34 repos.append((prefix, root))
60 repos.append((prefix, root))
35 continue
61 continue
36 roothead = os.path.normpath(os.path.abspath(roothead))
62 roothead = os.path.normpath(os.path.abspath(roothead))
37 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
63 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
38 repos.extend(urlrepos(prefix, roothead, paths))
64 repos.extend(urlrepos(prefix, roothead, paths))
39 return repos
65 return repos
40
66
41 def urlrepos(prefix, roothead, paths):
67 def urlrepos(prefix, roothead, paths):
42 """yield url paths and filesystem paths from a list of repo paths
68 """yield url paths and filesystem paths from a list of repo paths
43
69
44 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
70 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
45 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
71 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
46 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
72 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
47 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
73 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
48 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
74 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
49 """
75 """
50 for path in paths:
76 for path in paths:
51 path = os.path.normpath(path)
77 path = os.path.normpath(path)
52 yield (prefix + '/' +
78 yield (prefix + '/' +
53 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
79 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
54
80
55 def geturlcgivars(baseurl, port):
81 def geturlcgivars(baseurl, port):
56 """
82 """
57 Extract CGI variables from baseurl
83 Extract CGI variables from baseurl
58
84
59 >>> geturlcgivars("http://host.org/base", "80")
85 >>> geturlcgivars("http://host.org/base", "80")
60 ('host.org', '80', '/base')
86 ('host.org', '80', '/base')
61 >>> geturlcgivars("http://host.org:8000/base", "80")
87 >>> geturlcgivars("http://host.org:8000/base", "80")
62 ('host.org', '8000', '/base')
88 ('host.org', '8000', '/base')
63 >>> geturlcgivars('/base', 8000)
89 >>> geturlcgivars('/base', 8000)
64 ('', '8000', '/base')
90 ('', '8000', '/base')
65 >>> geturlcgivars("base", '8000')
91 >>> geturlcgivars("base", '8000')
66 ('', '8000', '/base')
92 ('', '8000', '/base')
67 >>> geturlcgivars("http://host", '8000')
93 >>> geturlcgivars("http://host", '8000')
68 ('host', '8000', '/')
94 ('host', '8000', '/')
69 >>> geturlcgivars("http://host/", '8000')
95 >>> geturlcgivars("http://host/", '8000')
70 ('host', '8000', '/')
96 ('host', '8000', '/')
71 """
97 """
72 u = util.url(baseurl)
98 u = util.url(baseurl)
73 name = u.host or ''
99 name = u.host or ''
74 if u.port:
100 if u.port:
75 port = u.port
101 port = u.port
76 path = u.path or ""
102 path = u.path or ""
77 if not path.startswith('/'):
103 if not path.startswith('/'):
78 path = '/' + path
104 path = '/' + path
79
105
80 return name, str(port), path
106 return name, str(port), path
81
107
82 class hgwebdir(object):
108 class hgwebdir(object):
83 """HTTP server for multiple repositories.
109 """HTTP server for multiple repositories.
84
110
85 Given a configuration, different repositories will be served depending
111 Given a configuration, different repositories will be served depending
86 on the request path.
112 on the request path.
87
113
88 Instances are typically used as WSGI applications.
114 Instances are typically used as WSGI applications.
89 """
115 """
90 def __init__(self, conf, baseui=None):
116 def __init__(self, conf, baseui=None):
91 self.conf = conf
117 self.conf = conf
92 self.baseui = baseui
118 self.baseui = baseui
93 self.ui = None
119 self.ui = None
94 self.lastrefresh = 0
120 self.lastrefresh = 0
95 self.motd = None
121 self.motd = None
96 self.refresh()
122 self.refresh()
97
123
98 def refresh(self):
124 def refresh(self):
99 refreshinterval = 20
125 refreshinterval = 20
100 if self.ui:
126 if self.ui:
101 refreshinterval = self.ui.configint('web', 'refreshinterval',
127 refreshinterval = self.ui.configint('web', 'refreshinterval',
102 refreshinterval)
128 refreshinterval)
103
129
104 # refreshinterval <= 0 means to always refresh.
130 # refreshinterval <= 0 means to always refresh.
105 if (refreshinterval > 0 and
131 if (refreshinterval > 0 and
106 self.lastrefresh + refreshinterval > time.time()):
132 self.lastrefresh + refreshinterval > time.time()):
107 return
133 return
108
134
109 if self.baseui:
135 if self.baseui:
110 u = self.baseui.copy()
136 u = self.baseui.copy()
111 else:
137 else:
112 u = uimod.ui()
138 u = uimod.ui()
113 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
139 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
114 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
140 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
115 # displaying bundling progress bar while serving feels wrong and may
141 # displaying bundling progress bar while serving feels wrong and may
116 # break some wsgi implementations.
142 # break some wsgi implementations.
117 u.setconfig('progress', 'disable', 'true', 'hgweb')
143 u.setconfig('progress', 'disable', 'true', 'hgweb')
118
144
119 if not isinstance(self.conf, (dict, list, tuple)):
145 if not isinstance(self.conf, (dict, list, tuple)):
120 map = {'paths': 'hgweb-paths'}
146 map = {'paths': 'hgweb-paths'}
121 if not os.path.exists(self.conf):
147 if not os.path.exists(self.conf):
122 raise error.Abort(_('config file %s not found!') % self.conf)
148 raise error.Abort(_('config file %s not found!') % self.conf)
123 u.readconfig(self.conf, remap=map, trust=True)
149 u.readconfig(self.conf, remap=map, trust=True)
124 paths = []
150 paths = []
125 for name, ignored in u.configitems('hgweb-paths'):
151 for name, ignored in u.configitems('hgweb-paths'):
126 for path in u.configlist('hgweb-paths', name):
152 for path in u.configlist('hgweb-paths', name):
127 paths.append((name, path))
153 paths.append((name, path))
128 elif isinstance(self.conf, (list, tuple)):
154 elif isinstance(self.conf, (list, tuple)):
129 paths = self.conf
155 paths = self.conf
130 elif isinstance(self.conf, dict):
156 elif isinstance(self.conf, dict):
131 paths = self.conf.items()
157 paths = self.conf.items()
132
158
133 repos = findrepos(paths)
159 repos = findrepos(paths)
134 for prefix, root in u.configitems('collections'):
160 for prefix, root in u.configitems('collections'):
135 prefix = util.pconvert(prefix)
161 prefix = util.pconvert(prefix)
136 for path in scmutil.walkrepos(root, followsym=True):
162 for path in scmutil.walkrepos(root, followsym=True):
137 repo = os.path.normpath(path)
163 repo = os.path.normpath(path)
138 name = util.pconvert(repo)
164 name = util.pconvert(repo)
139 if name.startswith(prefix):
165 if name.startswith(prefix):
140 name = name[len(prefix):]
166 name = name[len(prefix):]
141 repos.append((name.lstrip('/'), repo))
167 repos.append((name.lstrip('/'), repo))
142
168
143 self.repos = repos
169 self.repos = repos
144 self.ui = u
170 self.ui = u
145 encoding.encoding = self.ui.config('web', 'encoding',
171 encoding.encoding = self.ui.config('web', 'encoding',
146 encoding.encoding)
172 encoding.encoding)
147 self.style = self.ui.config('web', 'style', 'paper')
173 self.style = self.ui.config('web', 'style', 'paper')
148 self.templatepath = self.ui.config('web', 'templates', None)
174 self.templatepath = self.ui.config('web', 'templates', None)
149 self.stripecount = self.ui.config('web', 'stripes', 1)
175 self.stripecount = self.ui.config('web', 'stripes', 1)
150 if self.stripecount:
176 if self.stripecount:
151 self.stripecount = int(self.stripecount)
177 self.stripecount = int(self.stripecount)
152 self._baseurl = self.ui.config('web', 'baseurl')
178 self._baseurl = self.ui.config('web', 'baseurl')
153 prefix = self.ui.config('web', 'prefix', '')
179 prefix = self.ui.config('web', 'prefix', '')
154 if prefix.startswith('/'):
180 if prefix.startswith('/'):
155 prefix = prefix[1:]
181 prefix = prefix[1:]
156 if prefix.endswith('/'):
182 if prefix.endswith('/'):
157 prefix = prefix[:-1]
183 prefix = prefix[:-1]
158 self.prefix = prefix
184 self.prefix = prefix
159 self.lastrefresh = time.time()
185 self.lastrefresh = time.time()
160
186
161 def run(self):
187 def run(self):
162 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
188 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
163 raise RuntimeError("This function is only intended to be "
189 raise RuntimeError("This function is only intended to be "
164 "called while running as a CGI script.")
190 "called while running as a CGI script.")
165 wsgicgi.launch(self)
191 wsgicgi.launch(self)
166
192
167 def __call__(self, env, respond):
193 def __call__(self, env, respond):
168 req = wsgirequest(env, respond)
194 req = wsgirequest(env, respond)
169 return self.run_wsgi(req)
195 return self.run_wsgi(req)
170
196
171 def read_allowed(self, ui, req):
197 def read_allowed(self, ui, req):
172 """Check allow_read and deny_read config options of a repo's ui object
198 """Check allow_read and deny_read config options of a repo's ui object
173 to determine user permissions. By default, with neither option set (or
199 to determine user permissions. By default, with neither option set (or
174 both empty), allow all users to read the repo. There are two ways a
200 both empty), allow all users to read the repo. There are two ways a
175 user can be denied read access: (1) deny_read is not empty, and the
201 user can be denied read access: (1) deny_read is not empty, and the
176 user is unauthenticated or deny_read contains user (or *), and (2)
202 user is unauthenticated or deny_read contains user (or *), and (2)
177 allow_read is not empty and the user is not in allow_read. Return True
203 allow_read is not empty and the user is not in allow_read. Return True
178 if user is allowed to read the repo, else return False."""
204 if user is allowed to read the repo, else return False."""
179
205
180 user = req.env.get('REMOTE_USER')
206 user = req.env.get('REMOTE_USER')
181
207
182 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
208 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
183 if deny_read and (not user or ismember(ui, user, deny_read)):
209 if deny_read and (not user or ismember(ui, user, deny_read)):
184 return False
210 return False
185
211
186 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
212 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
187 # by default, allow reading if no allow_read option has been set
213 # by default, allow reading if no allow_read option has been set
188 if (not allow_read) or ismember(ui, user, allow_read):
214 if (not allow_read) or ismember(ui, user, allow_read):
189 return True
215 return True
190
216
191 return False
217 return False
192
218
193 def run_wsgi(self, req):
219 def run_wsgi(self, req):
194 try:
220 try:
195 self.refresh()
221 self.refresh()
196
222
197 virtual = req.env.get("PATH_INFO", "").strip('/')
223 virtual = req.env.get("PATH_INFO", "").strip('/')
198 tmpl = self.templater(req)
224 tmpl = self.templater(req)
199 ctype = tmpl('mimetype', encoding=encoding.encoding)
225 ctype = tmpl('mimetype', encoding=encoding.encoding)
200 ctype = templater.stringify(ctype)
226 ctype = templater.stringify(ctype)
201
227
202 # a static file
228 # a static file
203 if virtual.startswith('static/') or 'static' in req.form:
229 if virtual.startswith('static/') or 'static' in req.form:
204 if virtual.startswith('static/'):
230 if virtual.startswith('static/'):
205 fname = virtual[7:]
231 fname = virtual[7:]
206 else:
232 else:
207 fname = req.form['static'][0]
233 fname = req.form['static'][0]
208 static = self.ui.config("web", "static", None,
234 static = self.ui.config("web", "static", None,
209 untrusted=False)
235 untrusted=False)
210 if not static:
236 if not static:
211 tp = self.templatepath or templater.templatepaths()
237 tp = self.templatepath or templater.templatepaths()
212 if isinstance(tp, str):
238 if isinstance(tp, str):
213 tp = [tp]
239 tp = [tp]
214 static = [os.path.join(p, 'static') for p in tp]
240 static = [os.path.join(p, 'static') for p in tp]
215 staticfile(static, fname, req)
241 staticfile(static, fname, req)
216 return []
242 return []
217
243
218 # top-level index
244 # top-level index
219 elif not virtual:
245 elif not virtual:
220 req.respond(HTTP_OK, ctype)
246 req.respond(HTTP_OK, ctype)
221 return self.makeindex(req, tmpl)
247 return self.makeindex(req, tmpl)
222
248
223 # nested indexes and hgwebs
249 # nested indexes and hgwebs
224
250
225 repos = dict(self.repos)
251 repos = dict(self.repos)
226 virtualrepo = virtual
252 virtualrepo = virtual
227 while virtualrepo:
253 while virtualrepo:
228 real = repos.get(virtualrepo)
254 real = repos.get(virtualrepo)
229 if real:
255 if real:
230 req.env['REPO_NAME'] = virtualrepo
256 req.env['REPO_NAME'] = virtualrepo
231 try:
257 try:
232 # ensure caller gets private copy of ui
258 # ensure caller gets private copy of ui
233 repo = hg.repository(self.ui.copy(), real)
259 repo = hg.repository(self.ui.copy(), real)
234 return hgweb_mod.hgweb(repo).run_wsgi(req)
260 return hgweb_mod.hgweb(repo).run_wsgi(req)
235 except IOError as inst:
261 except IOError as inst:
236 msg = inst.strerror
262 msg = inst.strerror
237 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
263 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
238 except error.RepoError as inst:
264 except error.RepoError as inst:
239 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
265 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
240
266
241 up = virtualrepo.rfind('/')
267 up = virtualrepo.rfind('/')
242 if up < 0:
268 if up < 0:
243 break
269 break
244 virtualrepo = virtualrepo[:up]
270 virtualrepo = virtualrepo[:up]
245
271
246 # browse subdirectories
272 # browse subdirectories
247 subdir = virtual + '/'
273 subdir = virtual + '/'
248 if [r for r in repos if r.startswith(subdir)]:
274 if [r for r in repos if r.startswith(subdir)]:
249 req.respond(HTTP_OK, ctype)
275 req.respond(HTTP_OK, ctype)
250 return self.makeindex(req, tmpl, subdir)
276 return self.makeindex(req, tmpl, subdir)
251
277
252 # prefixes not found
278 # prefixes not found
253 req.respond(HTTP_NOT_FOUND, ctype)
279 req.respond(HTTP_NOT_FOUND, ctype)
254 return tmpl("notfound", repo=virtual)
280 return tmpl("notfound", repo=virtual)
255
281
256 except ErrorResponse as err:
282 except ErrorResponse as err:
257 req.respond(err, ctype)
283 req.respond(err, ctype)
258 return tmpl('error', error=err.message or '')
284 return tmpl('error', error=err.message or '')
259 finally:
285 finally:
260 tmpl = None
286 tmpl = None
261
287
262 def makeindex(self, req, tmpl, subdir=""):
288 def makeindex(self, req, tmpl, subdir=""):
263
289
264 def archivelist(ui, nodeid, url):
290 def archivelist(ui, nodeid, url):
265 allowed = ui.configlist("web", "allow_archive", untrusted=True)
291 allowed = ui.configlist("web", "allow_archive", untrusted=True)
266 archives = []
292 archives = []
267 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
293 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
268 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
294 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
269 untrusted=True):
295 untrusted=True):
270 archives.append({"type" : i[0], "extension": i[1],
296 archives.append({"type" : i[0], "extension": i[1],
271 "node": nodeid, "url": url})
297 "node": nodeid, "url": url})
272 return archives
298 return archives
273
299
274 def rawentries(subdir="", **map):
300 def rawentries(subdir="", **map):
275
301
276 descend = self.ui.configbool('web', 'descend', True)
302 descend = self.ui.configbool('web', 'descend', True)
277 collapse = self.ui.configbool('web', 'collapse', False)
303 collapse = self.ui.configbool('web', 'collapse', False)
278 seenrepos = set()
304 seenrepos = set()
279 seendirs = set()
305 seendirs = set()
280 for name, path in self.repos:
306 for name, path in self.repos:
281
307
282 if not name.startswith(subdir):
308 if not name.startswith(subdir):
283 continue
309 continue
284 name = name[len(subdir):]
310 name = name[len(subdir):]
285 directory = False
311 directory = False
286
312
287 if '/' in name:
313 if '/' in name:
288 if not descend:
314 if not descend:
289 continue
315 continue
290
316
291 nameparts = name.split('/')
317 nameparts = name.split('/')
292 rootname = nameparts[0]
318 rootname = nameparts[0]
293
319
294 if not collapse:
320 if not collapse:
295 pass
321 pass
296 elif rootname in seendirs:
322 elif rootname in seendirs:
297 continue
323 continue
298 elif rootname in seenrepos:
324 elif rootname in seenrepos:
299 pass
325 pass
300 else:
326 else:
301 directory = True
327 directory = True
302 name = rootname
328 name = rootname
303
329
304 # redefine the path to refer to the directory
330 # redefine the path to refer to the directory
305 discarded = '/'.join(nameparts[1:])
331 discarded = '/'.join(nameparts[1:])
306
332
307 # remove name parts plus accompanying slash
333 # remove name parts plus accompanying slash
308 path = path[:-len(discarded) - 1]
334 path = path[:-len(discarded) - 1]
309
335
310 try:
336 try:
311 r = hg.repository(self.ui, path)
337 r = hg.repository(self.ui, path)
312 directory = False
338 directory = False
313 except (IOError, error.RepoError):
339 except (IOError, error.RepoError):
314 pass
340 pass
315
341
316 parts = [name]
342 parts = [name]
317 if 'PATH_INFO' in req.env:
343 if 'PATH_INFO' in req.env:
318 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
344 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
319 if req.env['SCRIPT_NAME']:
345 if req.env['SCRIPT_NAME']:
320 parts.insert(0, req.env['SCRIPT_NAME'])
346 parts.insert(0, req.env['SCRIPT_NAME'])
321 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
347 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
322
348
323 # show either a directory entry or a repository
349 # show either a directory entry or a repository
324 if directory:
350 if directory:
325 # get the directory's time information
351 # get the directory's time information
326 try:
352 try:
327 d = (get_mtime(path), util.makedate()[1])
353 d = (get_mtime(path), util.makedate()[1])
328 except OSError:
354 except OSError:
329 continue
355 continue
330
356
331 # add '/' to the name to make it obvious that
357 # add '/' to the name to make it obvious that
332 # the entry is a directory, not a regular repository
358 # the entry is a directory, not a regular repository
333 row = {'contact': "",
359 row = {'contact': "",
334 'contact_sort': "",
360 'contact_sort': "",
335 'name': name + '/',
361 'name': name + '/',
336 'name_sort': name,
362 'name_sort': name,
337 'url': url,
363 'url': url,
338 'description': "",
364 'description': "",
339 'description_sort': "",
365 'description_sort': "",
340 'lastchange': d,
366 'lastchange': d,
341 'lastchange_sort': d[1]-d[0],
367 'lastchange_sort': d[1]-d[0],
342 'archives': [],
368 'archives': [],
343 'isdirectory': True}
369 'isdirectory': True}
344
370
345 seendirs.add(name)
371 seendirs.add(name)
346 yield row
372 yield row
347 continue
373 continue
348
374
349 u = self.ui.copy()
375 u = self.ui.copy()
350 try:
376 try:
351 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
377 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
352 except Exception as e:
378 except Exception as e:
353 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
379 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
354 continue
380 continue
355 def get(section, name, default=None):
381 def get(section, name, default=None):
356 return u.config(section, name, default, untrusted=True)
382 return u.config(section, name, default, untrusted=True)
357
383
358 if u.configbool("web", "hidden", untrusted=True):
384 if u.configbool("web", "hidden", untrusted=True):
359 continue
385 continue
360
386
361 if not self.read_allowed(u, req):
387 if not self.read_allowed(u, req):
362 continue
388 continue
363
389
364 # update time with local timezone
390 # update time with local timezone
365 try:
391 try:
366 r = hg.repository(self.ui, path)
392 r = hg.repository(self.ui, path)
367 except IOError:
393 except IOError:
368 u.warn(_('error accessing repository at %s\n') % path)
394 u.warn(_('error accessing repository at %s\n') % path)
369 continue
395 continue
370 except error.RepoError:
396 except error.RepoError:
371 u.warn(_('error accessing repository at %s\n') % path)
397 u.warn(_('error accessing repository at %s\n') % path)
372 continue
398 continue
373 try:
399 try:
374 d = (get_mtime(r.spath), util.makedate()[1])
400 d = (get_mtime(r.spath), util.makedate()[1])
375 except OSError:
401 except OSError:
376 continue
402 continue
377
403
378 contact = get_contact(get)
404 contact = get_contact(get)
379 description = get("web", "description", "")
405 description = get("web", "description", "")
380 seenrepos.add(name)
406 seenrepos.add(name)
381 name = get("web", "name", name)
407 name = get("web", "name", name)
382 row = {'contact': contact or "unknown",
408 row = {'contact': contact or "unknown",
383 'contact_sort': contact.upper() or "unknown",
409 'contact_sort': contact.upper() or "unknown",
384 'name': name,
410 'name': name,
385 'name_sort': name,
411 'name_sort': name,
386 'url': url,
412 'url': url,
387 'description': description or "unknown",
413 'description': description or "unknown",
388 'description_sort': description.upper() or "unknown",
414 'description_sort': description.upper() or "unknown",
389 'lastchange': d,
415 'lastchange': d,
390 'lastchange_sort': d[1]-d[0],
416 'lastchange_sort': d[1]-d[0],
391 'archives': archivelist(u, "tip", url),
417 'archives': archivelist(u, "tip", url),
392 'isdirectory': None,
418 'isdirectory': None,
393 }
419 }
394
420
395 yield row
421 yield row
396
422
397 sortdefault = None, False
423 sortdefault = None, False
398 def entries(sortcolumn="", descending=False, subdir="", **map):
424 def entries(sortcolumn="", descending=False, subdir="", **map):
399 rows = rawentries(subdir=subdir, **map)
425 rows = rawentries(subdir=subdir, **map)
400
426
401 if sortcolumn and sortdefault != (sortcolumn, descending):
427 if sortcolumn and sortdefault != (sortcolumn, descending):
402 sortkey = '%s_sort' % sortcolumn
428 sortkey = '%s_sort' % sortcolumn
403 rows = sorted(rows, key=lambda x: x[sortkey],
429 rows = sorted(rows, key=lambda x: x[sortkey],
404 reverse=descending)
430 reverse=descending)
405 for row, parity in zip(rows, paritygen(self.stripecount)):
431 for row, parity in zip(rows, paritygen(self.stripecount)):
406 row['parity'] = parity
432 row['parity'] = parity
407 yield row
433 yield row
408
434
409 self.refresh()
435 self.refresh()
410 sortable = ["name", "description", "contact", "lastchange"]
436 sortable = ["name", "description", "contact", "lastchange"]
411 sortcolumn, descending = sortdefault
437 sortcolumn, descending = sortdefault
412 if 'sort' in req.form:
438 if 'sort' in req.form:
413 sortcolumn = req.form['sort'][0]
439 sortcolumn = req.form['sort'][0]
414 descending = sortcolumn.startswith('-')
440 descending = sortcolumn.startswith('-')
415 if descending:
441 if descending:
416 sortcolumn = sortcolumn[1:]
442 sortcolumn = sortcolumn[1:]
417 if sortcolumn not in sortable:
443 if sortcolumn not in sortable:
418 sortcolumn = ""
444 sortcolumn = ""
419
445
420 sort = [("sort_%s" % column,
446 sort = [("sort_%s" % column,
421 "%s%s" % ((not descending and column == sortcolumn)
447 "%s%s" % ((not descending and column == sortcolumn)
422 and "-" or "", column))
448 and "-" or "", column))
423 for column in sortable]
449 for column in sortable]
424
450
425 self.refresh()
451 self.refresh()
426 self.updatereqenv(req.env)
452 self.updatereqenv(req.env)
427
453
428 return tmpl("index", entries=entries, subdir=subdir,
454 return tmpl("index", entries=entries, subdir=subdir,
429 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
455 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
430 sortcolumn=sortcolumn, descending=descending,
456 sortcolumn=sortcolumn, descending=descending,
431 **dict(sort))
457 **dict(sort))
432
458
433 def templater(self, req):
459 def templater(self, req):
434
460
435 def motd(**map):
461 def motd(**map):
436 if self.motd is not None:
462 if self.motd is not None:
437 yield self.motd
463 yield self.motd
438 else:
464 else:
439 yield config('web', 'motd', '')
465 yield config('web', 'motd', '')
440
466
441 def config(section, name, default=None, untrusted=True):
467 def config(section, name, default=None, untrusted=True):
442 return self.ui.config(section, name, default, untrusted)
468 return self.ui.config(section, name, default, untrusted)
443
469
444 self.updatereqenv(req.env)
470 self.updatereqenv(req.env)
445
471
446 url = req.env.get('SCRIPT_NAME', '')
472 url = req.env.get('SCRIPT_NAME', '')
447 if not url.endswith('/'):
473 if not url.endswith('/'):
448 url += '/'
474 url += '/'
449
475
450 vars = {}
476 vars = {}
451 styles = (
477 styles = (
452 req.form.get('style', [None])[0],
478 req.form.get('style', [None])[0],
453 config('web', 'style'),
479 config('web', 'style'),
454 'paper'
480 'paper'
455 )
481 )
456 style, mapfile = templater.stylemap(styles, self.templatepath)
482 style, mapfile = templater.stylemap(styles, self.templatepath)
457 if style == styles[0]:
483 if style == styles[0]:
458 vars['style'] = style
484 vars['style'] = style
459
485
460 start = url[-1] == '?' and '&' or '?'
486 start = url[-1] == '?' and '&' or '?'
461 sessionvars = webutil.sessionvars(vars, start)
487 sessionvars = webutil.sessionvars(vars, start)
462 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
488 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
463 logoimg = config('web', 'logoimg', 'hglogo.png')
489 logoimg = config('web', 'logoimg', 'hglogo.png')
464 staticurl = config('web', 'staticurl') or url + 'static/'
490 staticurl = config('web', 'staticurl') or url + 'static/'
465 if not staticurl.endswith('/'):
491 if not staticurl.endswith('/'):
466 staticurl += '/'
492 staticurl += '/'
467
493
468 tmpl = templater.templater(mapfile,
494 tmpl = templater.templater(mapfile,
469 defaults={"encoding": encoding.encoding,
495 defaults={"encoding": encoding.encoding,
470 "motd": motd,
496 "motd": motd,
471 "url": url,
497 "url": url,
472 "logourl": logourl,
498 "logourl": logourl,
473 "logoimg": logoimg,
499 "logoimg": logoimg,
474 "staticurl": staticurl,
500 "staticurl": staticurl,
475 "sessionvars": sessionvars,
501 "sessionvars": sessionvars,
476 "style": style,
502 "style": style,
477 })
503 })
478 return tmpl
504 return tmpl
479
505
480 def updatereqenv(self, env):
506 def updatereqenv(self, env):
481 if self._baseurl is not None:
507 if self._baseurl is not None:
482 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
508 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
483 env['SERVER_NAME'] = name
509 env['SERVER_NAME'] = name
484 env['SERVER_PORT'] = port
510 env['SERVER_PORT'] = port
485 env['SCRIPT_NAME'] = path
511 env['SCRIPT_NAME'] = path
@@ -1,98 +1,110 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
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 import cgi, cStringIO, zlib, urllib
8 from __future__ import absolute_import
9 from mercurial import util, wireproto
9
10 from common import HTTP_OK
10 import cStringIO
11 import cgi
12 import urllib
13 import zlib
14
15 from .common import (
16 HTTP_OK,
17 )
18
19 from .. import (
20 util,
21 wireproto,
22 )
11
23
12 HGTYPE = 'application/mercurial-0.1'
24 HGTYPE = 'application/mercurial-0.1'
13 HGERRTYPE = 'application/hg-error'
25 HGERRTYPE = 'application/hg-error'
14
26
15 class webproto(wireproto.abstractserverproto):
27 class webproto(wireproto.abstractserverproto):
16 def __init__(self, req, ui):
28 def __init__(self, req, ui):
17 self.req = req
29 self.req = req
18 self.response = ''
30 self.response = ''
19 self.ui = ui
31 self.ui = ui
20 def getargs(self, args):
32 def getargs(self, args):
21 knownargs = self._args()
33 knownargs = self._args()
22 data = {}
34 data = {}
23 keys = args.split()
35 keys = args.split()
24 for k in keys:
36 for k in keys:
25 if k == '*':
37 if k == '*':
26 star = {}
38 star = {}
27 for key in knownargs.keys():
39 for key in knownargs.keys():
28 if key != 'cmd' and key not in keys:
40 if key != 'cmd' and key not in keys:
29 star[key] = knownargs[key][0]
41 star[key] = knownargs[key][0]
30 data['*'] = star
42 data['*'] = star
31 else:
43 else:
32 data[k] = knownargs[k][0]
44 data[k] = knownargs[k][0]
33 return [data[k] for k in keys]
45 return [data[k] for k in keys]
34 def _args(self):
46 def _args(self):
35 args = self.req.form.copy()
47 args = self.req.form.copy()
36 chunks = []
48 chunks = []
37 i = 1
49 i = 1
38 while True:
50 while True:
39 h = self.req.env.get('HTTP_X_HGARG_' + str(i))
51 h = self.req.env.get('HTTP_X_HGARG_' + str(i))
40 if h is None:
52 if h is None:
41 break
53 break
42 chunks += [h]
54 chunks += [h]
43 i += 1
55 i += 1
44 args.update(cgi.parse_qs(''.join(chunks), keep_blank_values=True))
56 args.update(cgi.parse_qs(''.join(chunks), keep_blank_values=True))
45 return args
57 return args
46 def getfile(self, fp):
58 def getfile(self, fp):
47 length = int(self.req.env['CONTENT_LENGTH'])
59 length = int(self.req.env['CONTENT_LENGTH'])
48 for s in util.filechunkiter(self.req, limit=length):
60 for s in util.filechunkiter(self.req, limit=length):
49 fp.write(s)
61 fp.write(s)
50 def redirect(self):
62 def redirect(self):
51 self.oldio = self.ui.fout, self.ui.ferr
63 self.oldio = self.ui.fout, self.ui.ferr
52 self.ui.ferr = self.ui.fout = cStringIO.StringIO()
64 self.ui.ferr = self.ui.fout = cStringIO.StringIO()
53 def restore(self):
65 def restore(self):
54 val = self.ui.fout.getvalue()
66 val = self.ui.fout.getvalue()
55 self.ui.ferr, self.ui.fout = self.oldio
67 self.ui.ferr, self.ui.fout = self.oldio
56 return val
68 return val
57 def groupchunks(self, cg):
69 def groupchunks(self, cg):
58 z = zlib.compressobj()
70 z = zlib.compressobj()
59 while True:
71 while True:
60 chunk = cg.read(4096)
72 chunk = cg.read(4096)
61 if not chunk:
73 if not chunk:
62 break
74 break
63 yield z.compress(chunk)
75 yield z.compress(chunk)
64 yield z.flush()
76 yield z.flush()
65 def _client(self):
77 def _client(self):
66 return 'remote:%s:%s:%s' % (
78 return 'remote:%s:%s:%s' % (
67 self.req.env.get('wsgi.url_scheme') or 'http',
79 self.req.env.get('wsgi.url_scheme') or 'http',
68 urllib.quote(self.req.env.get('REMOTE_HOST', '')),
80 urllib.quote(self.req.env.get('REMOTE_HOST', '')),
69 urllib.quote(self.req.env.get('REMOTE_USER', '')))
81 urllib.quote(self.req.env.get('REMOTE_USER', '')))
70
82
71 def iscmd(cmd):
83 def iscmd(cmd):
72 return cmd in wireproto.commands
84 return cmd in wireproto.commands
73
85
74 def call(repo, req, cmd):
86 def call(repo, req, cmd):
75 p = webproto(req, repo.ui)
87 p = webproto(req, repo.ui)
76 rsp = wireproto.dispatch(repo, p, cmd)
88 rsp = wireproto.dispatch(repo, p, cmd)
77 if isinstance(rsp, str):
89 if isinstance(rsp, str):
78 req.respond(HTTP_OK, HGTYPE, body=rsp)
90 req.respond(HTTP_OK, HGTYPE, body=rsp)
79 return []
91 return []
80 elif isinstance(rsp, wireproto.streamres):
92 elif isinstance(rsp, wireproto.streamres):
81 req.respond(HTTP_OK, HGTYPE)
93 req.respond(HTTP_OK, HGTYPE)
82 return rsp.gen
94 return rsp.gen
83 elif isinstance(rsp, wireproto.pushres):
95 elif isinstance(rsp, wireproto.pushres):
84 val = p.restore()
96 val = p.restore()
85 rsp = '%d\n%s' % (rsp.res, val)
97 rsp = '%d\n%s' % (rsp.res, val)
86 req.respond(HTTP_OK, HGTYPE, body=rsp)
98 req.respond(HTTP_OK, HGTYPE, body=rsp)
87 return []
99 return []
88 elif isinstance(rsp, wireproto.pusherr):
100 elif isinstance(rsp, wireproto.pusherr):
89 # drain the incoming bundle
101 # drain the incoming bundle
90 req.drain()
102 req.drain()
91 p.restore()
103 p.restore()
92 rsp = '0\n%s\n' % rsp.res
104 rsp = '0\n%s\n' % rsp.res
93 req.respond(HTTP_OK, HGTYPE, body=rsp)
105 req.respond(HTTP_OK, HGTYPE, body=rsp)
94 return []
106 return []
95 elif isinstance(rsp, wireproto.ooberror):
107 elif isinstance(rsp, wireproto.ooberror):
96 rsp = rsp.message
108 rsp = rsp.message
97 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
109 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
98 return []
110 return []
@@ -1,140 +1,152 b''
1 # hgweb/request.py - An http request from either CGI or the standalone server.
1 # hgweb/request.py - An http request from either CGI or the standalone server.
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 socket, cgi, errno
9 from __future__ import absolute_import
10 from mercurial import util
10
11 from common import ErrorResponse, statusmessage, HTTP_NOT_MODIFIED
11 import cgi
12 import errno
13 import socket
14
15 from .common import (
16 ErrorResponse,
17 HTTP_NOT_MODIFIED,
18 statusmessage,
19 )
20
21 from .. import (
22 util,
23 )
12
24
13 shortcuts = {
25 shortcuts = {
14 'cl': [('cmd', ['changelog']), ('rev', None)],
26 'cl': [('cmd', ['changelog']), ('rev', None)],
15 'sl': [('cmd', ['shortlog']), ('rev', None)],
27 'sl': [('cmd', ['shortlog']), ('rev', None)],
16 'cs': [('cmd', ['changeset']), ('node', None)],
28 'cs': [('cmd', ['changeset']), ('node', None)],
17 'f': [('cmd', ['file']), ('filenode', None)],
29 'f': [('cmd', ['file']), ('filenode', None)],
18 'fl': [('cmd', ['filelog']), ('filenode', None)],
30 'fl': [('cmd', ['filelog']), ('filenode', None)],
19 'fd': [('cmd', ['filediff']), ('node', None)],
31 'fd': [('cmd', ['filediff']), ('node', None)],
20 'fa': [('cmd', ['annotate']), ('filenode', None)],
32 'fa': [('cmd', ['annotate']), ('filenode', None)],
21 'mf': [('cmd', ['manifest']), ('manifest', None)],
33 'mf': [('cmd', ['manifest']), ('manifest', None)],
22 'ca': [('cmd', ['archive']), ('node', None)],
34 'ca': [('cmd', ['archive']), ('node', None)],
23 'tags': [('cmd', ['tags'])],
35 'tags': [('cmd', ['tags'])],
24 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
36 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
25 'static': [('cmd', ['static']), ('file', None)]
37 'static': [('cmd', ['static']), ('file', None)]
26 }
38 }
27
39
28 def normalize(form):
40 def normalize(form):
29 # first expand the shortcuts
41 # first expand the shortcuts
30 for k in shortcuts.iterkeys():
42 for k in shortcuts.iterkeys():
31 if k in form:
43 if k in form:
32 for name, value in shortcuts[k]:
44 for name, value in shortcuts[k]:
33 if value is None:
45 if value is None:
34 value = form[k]
46 value = form[k]
35 form[name] = value
47 form[name] = value
36 del form[k]
48 del form[k]
37 # And strip the values
49 # And strip the values
38 for k, v in form.iteritems():
50 for k, v in form.iteritems():
39 form[k] = [i.strip() for i in v]
51 form[k] = [i.strip() for i in v]
40 return form
52 return form
41
53
42 class wsgirequest(object):
54 class wsgirequest(object):
43 """Higher-level API for a WSGI request.
55 """Higher-level API for a WSGI request.
44
56
45 WSGI applications are invoked with 2 arguments. They are used to
57 WSGI applications are invoked with 2 arguments. They are used to
46 instantiate instances of this class, which provides higher-level APIs
58 instantiate instances of this class, which provides higher-level APIs
47 for obtaining request parameters, writing HTTP output, etc.
59 for obtaining request parameters, writing HTTP output, etc.
48 """
60 """
49 def __init__(self, wsgienv, start_response):
61 def __init__(self, wsgienv, start_response):
50 version = wsgienv['wsgi.version']
62 version = wsgienv['wsgi.version']
51 if (version < (1, 0)) or (version >= (2, 0)):
63 if (version < (1, 0)) or (version >= (2, 0)):
52 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
64 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
53 % version)
65 % version)
54 self.inp = wsgienv['wsgi.input']
66 self.inp = wsgienv['wsgi.input']
55 self.err = wsgienv['wsgi.errors']
67 self.err = wsgienv['wsgi.errors']
56 self.threaded = wsgienv['wsgi.multithread']
68 self.threaded = wsgienv['wsgi.multithread']
57 self.multiprocess = wsgienv['wsgi.multiprocess']
69 self.multiprocess = wsgienv['wsgi.multiprocess']
58 self.run_once = wsgienv['wsgi.run_once']
70 self.run_once = wsgienv['wsgi.run_once']
59 self.env = wsgienv
71 self.env = wsgienv
60 self.form = normalize(cgi.parse(self.inp,
72 self.form = normalize(cgi.parse(self.inp,
61 self.env,
73 self.env,
62 keep_blank_values=1))
74 keep_blank_values=1))
63 self._start_response = start_response
75 self._start_response = start_response
64 self.server_write = None
76 self.server_write = None
65 self.headers = []
77 self.headers = []
66
78
67 def __iter__(self):
79 def __iter__(self):
68 return iter([])
80 return iter([])
69
81
70 def read(self, count=-1):
82 def read(self, count=-1):
71 return self.inp.read(count)
83 return self.inp.read(count)
72
84
73 def drain(self):
85 def drain(self):
74 '''need to read all data from request, httplib is half-duplex'''
86 '''need to read all data from request, httplib is half-duplex'''
75 length = int(self.env.get('CONTENT_LENGTH') or 0)
87 length = int(self.env.get('CONTENT_LENGTH') or 0)
76 for s in util.filechunkiter(self.inp, limit=length):
88 for s in util.filechunkiter(self.inp, limit=length):
77 pass
89 pass
78
90
79 def respond(self, status, type, filename=None, body=None):
91 def respond(self, status, type, filename=None, body=None):
80 if self._start_response is not None:
92 if self._start_response is not None:
81 self.headers.append(('Content-Type', type))
93 self.headers.append(('Content-Type', type))
82 if filename:
94 if filename:
83 filename = (filename.rpartition('/')[-1]
95 filename = (filename.rpartition('/')[-1]
84 .replace('\\', '\\\\').replace('"', '\\"'))
96 .replace('\\', '\\\\').replace('"', '\\"'))
85 self.headers.append(('Content-Disposition',
97 self.headers.append(('Content-Disposition',
86 'inline; filename="%s"' % filename))
98 'inline; filename="%s"' % filename))
87 if body is not None:
99 if body is not None:
88 self.headers.append(('Content-Length', str(len(body))))
100 self.headers.append(('Content-Length', str(len(body))))
89
101
90 for k, v in self.headers:
102 for k, v in self.headers:
91 if not isinstance(v, str):
103 if not isinstance(v, str):
92 raise TypeError('header value must be string: %r' % (v,))
104 raise TypeError('header value must be string: %r' % (v,))
93
105
94 if isinstance(status, ErrorResponse):
106 if isinstance(status, ErrorResponse):
95 self.headers.extend(status.headers)
107 self.headers.extend(status.headers)
96 if status.code == HTTP_NOT_MODIFIED:
108 if status.code == HTTP_NOT_MODIFIED:
97 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
109 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
98 # it MUST NOT include any headers other than these and no
110 # it MUST NOT include any headers other than these and no
99 # body
111 # body
100 self.headers = [(k, v) for (k, v) in self.headers if
112 self.headers = [(k, v) for (k, v) in self.headers if
101 k in ('Date', 'ETag', 'Expires',
113 k in ('Date', 'ETag', 'Expires',
102 'Cache-Control', 'Vary')]
114 'Cache-Control', 'Vary')]
103 status = statusmessage(status.code, str(status))
115 status = statusmessage(status.code, str(status))
104 elif status == 200:
116 elif status == 200:
105 status = '200 Script output follows'
117 status = '200 Script output follows'
106 elif isinstance(status, int):
118 elif isinstance(status, int):
107 status = statusmessage(status)
119 status = statusmessage(status)
108
120
109 self.server_write = self._start_response(status, self.headers)
121 self.server_write = self._start_response(status, self.headers)
110 self._start_response = None
122 self._start_response = None
111 self.headers = []
123 self.headers = []
112 if body is not None:
124 if body is not None:
113 self.write(body)
125 self.write(body)
114 self.server_write = None
126 self.server_write = None
115
127
116 def write(self, thing):
128 def write(self, thing):
117 if thing:
129 if thing:
118 try:
130 try:
119 self.server_write(thing)
131 self.server_write(thing)
120 except socket.error as inst:
132 except socket.error as inst:
121 if inst[0] != errno.ECONNRESET:
133 if inst[0] != errno.ECONNRESET:
122 raise
134 raise
123
135
124 def writelines(self, lines):
136 def writelines(self, lines):
125 for line in lines:
137 for line in lines:
126 self.write(line)
138 self.write(line)
127
139
128 def flush(self):
140 def flush(self):
129 return None
141 return None
130
142
131 def close(self):
143 def close(self):
132 return None
144 return None
133
145
134 def wsgiapplication(app_maker):
146 def wsgiapplication(app_maker):
135 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
147 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
136 can and should now be used as a WSGI application.'''
148 can and should now be used as a WSGI application.'''
137 application = app_maker()
149 application = app_maker()
138 def run_wsgi(env, respond):
150 def run_wsgi(env, respond):
139 return application(env, respond)
151 return application(env, respond)
140 return run_wsgi
152 return run_wsgi
@@ -1,305 +1,322 b''
1 # hgweb/server.py - The standalone hg web server.
1 # hgweb/server.py - The standalone hg web server.
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-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 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, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
9 from __future__ import absolute_import
10 from mercurial import util, error
10
11 from mercurial.hgweb import common
11 import BaseHTTPServer
12 from mercurial.i18n import _
12 import SocketServer
13 import errno
14 import os
15 import socket
16 import sys
17 import traceback
18 import urllib
19
20 from ..i18n import _
21
22 from .. import (
23 error,
24 util,
25 )
26
27 from . import (
28 common,
29 )
13
30
14 def _splitURI(uri):
31 def _splitURI(uri):
15 """Return path and query that has been split from uri
32 """Return path and query that has been split from uri
16
33
17 Just like CGI environment, the path is unquoted, the query is
34 Just like CGI environment, the path is unquoted, the query is
18 not.
35 not.
19 """
36 """
20 if '?' in uri:
37 if '?' in uri:
21 path, query = uri.split('?', 1)
38 path, query = uri.split('?', 1)
22 else:
39 else:
23 path, query = uri, ''
40 path, query = uri, ''
24 return urllib.unquote(path), query
41 return urllib.unquote(path), query
25
42
26 class _error_logger(object):
43 class _error_logger(object):
27 def __init__(self, handler):
44 def __init__(self, handler):
28 self.handler = handler
45 self.handler = handler
29 def flush(self):
46 def flush(self):
30 pass
47 pass
31 def write(self, str):
48 def write(self, str):
32 self.writelines(str.split('\n'))
49 self.writelines(str.split('\n'))
33 def writelines(self, seq):
50 def writelines(self, seq):
34 for msg in seq:
51 for msg in seq:
35 self.handler.log_error("HG error: %s", msg)
52 self.handler.log_error("HG error: %s", msg)
36
53
37 class _httprequesthandler(BaseHTTPServer.BaseHTTPRequestHandler):
54 class _httprequesthandler(BaseHTTPServer.BaseHTTPRequestHandler):
38
55
39 url_scheme = 'http'
56 url_scheme = 'http'
40
57
41 @staticmethod
58 @staticmethod
42 def preparehttpserver(httpserver, ssl_cert):
59 def preparehttpserver(httpserver, ssl_cert):
43 """Prepare .socket of new HTTPServer instance"""
60 """Prepare .socket of new HTTPServer instance"""
44 pass
61 pass
45
62
46 def __init__(self, *args, **kargs):
63 def __init__(self, *args, **kargs):
47 self.protocol_version = 'HTTP/1.1'
64 self.protocol_version = 'HTTP/1.1'
48 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
65 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
49
66
50 def _log_any(self, fp, format, *args):
67 def _log_any(self, fp, format, *args):
51 fp.write("%s - - [%s] %s\n" % (self.client_address[0],
68 fp.write("%s - - [%s] %s\n" % (self.client_address[0],
52 self.log_date_time_string(),
69 self.log_date_time_string(),
53 format % args))
70 format % args))
54 fp.flush()
71 fp.flush()
55
72
56 def log_error(self, format, *args):
73 def log_error(self, format, *args):
57 self._log_any(self.server.errorlog, format, *args)
74 self._log_any(self.server.errorlog, format, *args)
58
75
59 def log_message(self, format, *args):
76 def log_message(self, format, *args):
60 self._log_any(self.server.accesslog, format, *args)
77 self._log_any(self.server.accesslog, format, *args)
61
78
62 def log_request(self, code='-', size='-'):
79 def log_request(self, code='-', size='-'):
63 xheaders = []
80 xheaders = []
64 if util.safehasattr(self, 'headers'):
81 if util.safehasattr(self, 'headers'):
65 xheaders = [h for h in self.headers.items()
82 xheaders = [h for h in self.headers.items()
66 if h[0].startswith('x-')]
83 if h[0].startswith('x-')]
67 self.log_message('"%s" %s %s%s',
84 self.log_message('"%s" %s %s%s',
68 self.requestline, str(code), str(size),
85 self.requestline, str(code), str(size),
69 ''.join([' %s:%s' % h for h in sorted(xheaders)]))
86 ''.join([' %s:%s' % h for h in sorted(xheaders)]))
70
87
71 def do_write(self):
88 def do_write(self):
72 try:
89 try:
73 self.do_hgweb()
90 self.do_hgweb()
74 except socket.error as inst:
91 except socket.error as inst:
75 if inst[0] != errno.EPIPE:
92 if inst[0] != errno.EPIPE:
76 raise
93 raise
77
94
78 def do_POST(self):
95 def do_POST(self):
79 try:
96 try:
80 self.do_write()
97 self.do_write()
81 except Exception:
98 except Exception:
82 self._start_response("500 Internal Server Error", [])
99 self._start_response("500 Internal Server Error", [])
83 self._write("Internal Server Error")
100 self._write("Internal Server Error")
84 self._done()
101 self._done()
85 tb = "".join(traceback.format_exception(*sys.exc_info()))
102 tb = "".join(traceback.format_exception(*sys.exc_info()))
86 self.log_error("Exception happened during processing "
103 self.log_error("Exception happened during processing "
87 "request '%s':\n%s", self.path, tb)
104 "request '%s':\n%s", self.path, tb)
88
105
89 def do_GET(self):
106 def do_GET(self):
90 self.do_POST()
107 self.do_POST()
91
108
92 def do_hgweb(self):
109 def do_hgweb(self):
93 path, query = _splitURI(self.path)
110 path, query = _splitURI(self.path)
94
111
95 env = {}
112 env = {}
96 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
113 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
97 env['REQUEST_METHOD'] = self.command
114 env['REQUEST_METHOD'] = self.command
98 env['SERVER_NAME'] = self.server.server_name
115 env['SERVER_NAME'] = self.server.server_name
99 env['SERVER_PORT'] = str(self.server.server_port)
116 env['SERVER_PORT'] = str(self.server.server_port)
100 env['REQUEST_URI'] = self.path
117 env['REQUEST_URI'] = self.path
101 env['SCRIPT_NAME'] = self.server.prefix
118 env['SCRIPT_NAME'] = self.server.prefix
102 env['PATH_INFO'] = path[len(self.server.prefix):]
119 env['PATH_INFO'] = path[len(self.server.prefix):]
103 env['REMOTE_HOST'] = self.client_address[0]
120 env['REMOTE_HOST'] = self.client_address[0]
104 env['REMOTE_ADDR'] = self.client_address[0]
121 env['REMOTE_ADDR'] = self.client_address[0]
105 if query:
122 if query:
106 env['QUERY_STRING'] = query
123 env['QUERY_STRING'] = query
107
124
108 if self.headers.typeheader is None:
125 if self.headers.typeheader is None:
109 env['CONTENT_TYPE'] = self.headers.type
126 env['CONTENT_TYPE'] = self.headers.type
110 else:
127 else:
111 env['CONTENT_TYPE'] = self.headers.typeheader
128 env['CONTENT_TYPE'] = self.headers.typeheader
112 length = self.headers.getheader('content-length')
129 length = self.headers.getheader('content-length')
113 if length:
130 if length:
114 env['CONTENT_LENGTH'] = length
131 env['CONTENT_LENGTH'] = length
115 for header in [h for h in self.headers.keys()
132 for header in [h for h in self.headers.keys()
116 if h not in ('content-type', 'content-length')]:
133 if h not in ('content-type', 'content-length')]:
117 hkey = 'HTTP_' + header.replace('-', '_').upper()
134 hkey = 'HTTP_' + header.replace('-', '_').upper()
118 hval = self.headers.getheader(header)
135 hval = self.headers.getheader(header)
119 hval = hval.replace('\n', '').strip()
136 hval = hval.replace('\n', '').strip()
120 if hval:
137 if hval:
121 env[hkey] = hval
138 env[hkey] = hval
122 env['SERVER_PROTOCOL'] = self.request_version
139 env['SERVER_PROTOCOL'] = self.request_version
123 env['wsgi.version'] = (1, 0)
140 env['wsgi.version'] = (1, 0)
124 env['wsgi.url_scheme'] = self.url_scheme
141 env['wsgi.url_scheme'] = self.url_scheme
125 if env.get('HTTP_EXPECT', '').lower() == '100-continue':
142 if env.get('HTTP_EXPECT', '').lower() == '100-continue':
126 self.rfile = common.continuereader(self.rfile, self.wfile.write)
143 self.rfile = common.continuereader(self.rfile, self.wfile.write)
127
144
128 env['wsgi.input'] = self.rfile
145 env['wsgi.input'] = self.rfile
129 env['wsgi.errors'] = _error_logger(self)
146 env['wsgi.errors'] = _error_logger(self)
130 env['wsgi.multithread'] = isinstance(self.server,
147 env['wsgi.multithread'] = isinstance(self.server,
131 SocketServer.ThreadingMixIn)
148 SocketServer.ThreadingMixIn)
132 env['wsgi.multiprocess'] = isinstance(self.server,
149 env['wsgi.multiprocess'] = isinstance(self.server,
133 SocketServer.ForkingMixIn)
150 SocketServer.ForkingMixIn)
134 env['wsgi.run_once'] = 0
151 env['wsgi.run_once'] = 0
135
152
136 self.saved_status = None
153 self.saved_status = None
137 self.saved_headers = []
154 self.saved_headers = []
138 self.sent_headers = False
155 self.sent_headers = False
139 self.length = None
156 self.length = None
140 self._chunked = None
157 self._chunked = None
141 for chunk in self.server.application(env, self._start_response):
158 for chunk in self.server.application(env, self._start_response):
142 self._write(chunk)
159 self._write(chunk)
143 if not self.sent_headers:
160 if not self.sent_headers:
144 self.send_headers()
161 self.send_headers()
145 self._done()
162 self._done()
146
163
147 def send_headers(self):
164 def send_headers(self):
148 if not self.saved_status:
165 if not self.saved_status:
149 raise AssertionError("Sending headers before "
166 raise AssertionError("Sending headers before "
150 "start_response() called")
167 "start_response() called")
151 saved_status = self.saved_status.split(None, 1)
168 saved_status = self.saved_status.split(None, 1)
152 saved_status[0] = int(saved_status[0])
169 saved_status[0] = int(saved_status[0])
153 self.send_response(*saved_status)
170 self.send_response(*saved_status)
154 self.length = None
171 self.length = None
155 self._chunked = False
172 self._chunked = False
156 for h in self.saved_headers:
173 for h in self.saved_headers:
157 self.send_header(*h)
174 self.send_header(*h)
158 if h[0].lower() == 'content-length':
175 if h[0].lower() == 'content-length':
159 self.length = int(h[1])
176 self.length = int(h[1])
160 if (self.length is None and
177 if (self.length is None and
161 saved_status[0] != common.HTTP_NOT_MODIFIED):
178 saved_status[0] != common.HTTP_NOT_MODIFIED):
162 self._chunked = (not self.close_connection and
179 self._chunked = (not self.close_connection and
163 self.request_version == "HTTP/1.1")
180 self.request_version == "HTTP/1.1")
164 if self._chunked:
181 if self._chunked:
165 self.send_header('Transfer-Encoding', 'chunked')
182 self.send_header('Transfer-Encoding', 'chunked')
166 else:
183 else:
167 self.send_header('Connection', 'close')
184 self.send_header('Connection', 'close')
168 self.end_headers()
185 self.end_headers()
169 self.sent_headers = True
186 self.sent_headers = True
170
187
171 def _start_response(self, http_status, headers, exc_info=None):
188 def _start_response(self, http_status, headers, exc_info=None):
172 code, msg = http_status.split(None, 1)
189 code, msg = http_status.split(None, 1)
173 code = int(code)
190 code = int(code)
174 self.saved_status = http_status
191 self.saved_status = http_status
175 bad_headers = ('connection', 'transfer-encoding')
192 bad_headers = ('connection', 'transfer-encoding')
176 self.saved_headers = [h for h in headers
193 self.saved_headers = [h for h in headers
177 if h[0].lower() not in bad_headers]
194 if h[0].lower() not in bad_headers]
178 return self._write
195 return self._write
179
196
180 def _write(self, data):
197 def _write(self, data):
181 if not self.saved_status:
198 if not self.saved_status:
182 raise AssertionError("data written before start_response() called")
199 raise AssertionError("data written before start_response() called")
183 elif not self.sent_headers:
200 elif not self.sent_headers:
184 self.send_headers()
201 self.send_headers()
185 if self.length is not None:
202 if self.length is not None:
186 if len(data) > self.length:
203 if len(data) > self.length:
187 raise AssertionError("Content-length header sent, but more "
204 raise AssertionError("Content-length header sent, but more "
188 "bytes than specified are being written.")
205 "bytes than specified are being written.")
189 self.length = self.length - len(data)
206 self.length = self.length - len(data)
190 elif self._chunked and data:
207 elif self._chunked and data:
191 data = '%x\r\n%s\r\n' % (len(data), data)
208 data = '%x\r\n%s\r\n' % (len(data), data)
192 self.wfile.write(data)
209 self.wfile.write(data)
193 self.wfile.flush()
210 self.wfile.flush()
194
211
195 def _done(self):
212 def _done(self):
196 if self._chunked:
213 if self._chunked:
197 self.wfile.write('0\r\n\r\n')
214 self.wfile.write('0\r\n\r\n')
198 self.wfile.flush()
215 self.wfile.flush()
199
216
200 class _httprequesthandlerssl(_httprequesthandler):
217 class _httprequesthandlerssl(_httprequesthandler):
201 """HTTPS handler based on Python's ssl module"""
218 """HTTPS handler based on Python's ssl module"""
202
219
203 url_scheme = 'https'
220 url_scheme = 'https'
204
221
205 @staticmethod
222 @staticmethod
206 def preparehttpserver(httpserver, ssl_cert):
223 def preparehttpserver(httpserver, ssl_cert):
207 try:
224 try:
208 import ssl
225 import ssl
209 ssl.wrap_socket
226 ssl.wrap_socket
210 except ImportError:
227 except ImportError:
211 raise error.Abort(_("SSL support is unavailable"))
228 raise error.Abort(_("SSL support is unavailable"))
212 httpserver.socket = ssl.wrap_socket(
229 httpserver.socket = ssl.wrap_socket(
213 httpserver.socket, server_side=True,
230 httpserver.socket, server_side=True,
214 certfile=ssl_cert, ssl_version=ssl.PROTOCOL_TLSv1)
231 certfile=ssl_cert, ssl_version=ssl.PROTOCOL_TLSv1)
215
232
216 def setup(self):
233 def setup(self):
217 self.connection = self.request
234 self.connection = self.request
218 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
235 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
219 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
236 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
220
237
221 try:
238 try:
222 from threading import activeCount
239 import threading
223 activeCount() # silence pyflakes
240 threading.activeCount() # silence pyflakes and bypass demandimport
224 _mixin = SocketServer.ThreadingMixIn
241 _mixin = SocketServer.ThreadingMixIn
225 except ImportError:
242 except ImportError:
226 if util.safehasattr(os, "fork"):
243 if util.safehasattr(os, "fork"):
227 _mixin = SocketServer.ForkingMixIn
244 _mixin = SocketServer.ForkingMixIn
228 else:
245 else:
229 class _mixin(object):
246 class _mixin(object):
230 pass
247 pass
231
248
232 def openlog(opt, default):
249 def openlog(opt, default):
233 if opt and opt != '-':
250 if opt and opt != '-':
234 return open(opt, 'a')
251 return open(opt, 'a')
235 return default
252 return default
236
253
237 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
254 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
238
255
239 # SO_REUSEADDR has broken semantics on windows
256 # SO_REUSEADDR has broken semantics on windows
240 if os.name == 'nt':
257 if os.name == 'nt':
241 allow_reuse_address = 0
258 allow_reuse_address = 0
242
259
243 def __init__(self, ui, app, addr, handler, **kwargs):
260 def __init__(self, ui, app, addr, handler, **kwargs):
244 BaseHTTPServer.HTTPServer.__init__(self, addr, handler, **kwargs)
261 BaseHTTPServer.HTTPServer.__init__(self, addr, handler, **kwargs)
245 self.daemon_threads = True
262 self.daemon_threads = True
246 self.application = app
263 self.application = app
247
264
248 handler.preparehttpserver(self, ui.config('web', 'certificate'))
265 handler.preparehttpserver(self, ui.config('web', 'certificate'))
249
266
250 prefix = ui.config('web', 'prefix', '')
267 prefix = ui.config('web', 'prefix', '')
251 if prefix:
268 if prefix:
252 prefix = '/' + prefix.strip('/')
269 prefix = '/' + prefix.strip('/')
253 self.prefix = prefix
270 self.prefix = prefix
254
271
255 alog = openlog(ui.config('web', 'accesslog', '-'), sys.stdout)
272 alog = openlog(ui.config('web', 'accesslog', '-'), sys.stdout)
256 elog = openlog(ui.config('web', 'errorlog', '-'), sys.stderr)
273 elog = openlog(ui.config('web', 'errorlog', '-'), sys.stderr)
257 self.accesslog = alog
274 self.accesslog = alog
258 self.errorlog = elog
275 self.errorlog = elog
259
276
260 self.addr, self.port = self.socket.getsockname()[0:2]
277 self.addr, self.port = self.socket.getsockname()[0:2]
261 self.fqaddr = socket.getfqdn(addr[0])
278 self.fqaddr = socket.getfqdn(addr[0])
262
279
263 class IPv6HTTPServer(MercurialHTTPServer):
280 class IPv6HTTPServer(MercurialHTTPServer):
264 address_family = getattr(socket, 'AF_INET6', None)
281 address_family = getattr(socket, 'AF_INET6', None)
265 def __init__(self, *args, **kwargs):
282 def __init__(self, *args, **kwargs):
266 if self.address_family is None:
283 if self.address_family is None:
267 raise error.RepoError(_('IPv6 is not available on this system'))
284 raise error.RepoError(_('IPv6 is not available on this system'))
268 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
285 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
269
286
270 def create_server(ui, app):
287 def create_server(ui, app):
271
288
272 if ui.config('web', 'certificate'):
289 if ui.config('web', 'certificate'):
273 handler = _httprequesthandlerssl
290 handler = _httprequesthandlerssl
274 else:
291 else:
275 handler = _httprequesthandler
292 handler = _httprequesthandler
276
293
277 if ui.configbool('web', 'ipv6'):
294 if ui.configbool('web', 'ipv6'):
278 cls = IPv6HTTPServer
295 cls = IPv6HTTPServer
279 else:
296 else:
280 cls = MercurialHTTPServer
297 cls = MercurialHTTPServer
281
298
282 # ugly hack due to python issue5853 (for threaded use)
299 # ugly hack due to python issue5853 (for threaded use)
283 try:
300 try:
284 import mimetypes
301 import mimetypes
285 mimetypes.init()
302 mimetypes.init()
286 except UnicodeDecodeError:
303 except UnicodeDecodeError:
287 # Python 2.x's mimetypes module attempts to decode strings
304 # Python 2.x's mimetypes module attempts to decode strings
288 # from Windows' ANSI APIs as ascii (fail), then re-encode them
305 # from Windows' ANSI APIs as ascii (fail), then re-encode them
289 # as ascii (clown fail), because the default Python Unicode
306 # as ascii (clown fail), because the default Python Unicode
290 # codec is hardcoded as ascii.
307 # codec is hardcoded as ascii.
291
308
292 sys.argv # unwrap demand-loader so that reload() works
309 sys.argv # unwrap demand-loader so that reload() works
293 reload(sys) # resurrect sys.setdefaultencoding()
310 reload(sys) # resurrect sys.setdefaultencoding()
294 oldenc = sys.getdefaultencoding()
311 oldenc = sys.getdefaultencoding()
295 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
312 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
296 mimetypes.init()
313 mimetypes.init()
297 sys.setdefaultencoding(oldenc)
314 sys.setdefaultencoding(oldenc)
298
315
299 address = ui.config('web', 'address', '')
316 address = ui.config('web', 'address', '')
300 port = util.getport(ui.config('web', 'port', 8000))
317 port = util.getport(ui.config('web', 'port', 8000))
301 try:
318 try:
302 return cls(ui, app, (address, port), handler)
319 return cls(ui, app, (address, port), handler)
303 except socket.error as inst:
320 except socket.error as inst:
304 raise error.Abort(_("cannot start server at '%s:%d': %s")
321 raise error.Abort(_("cannot start server at '%s:%d': %s")
305 % (address, port, inst.args[1]))
322 % (address, port, inst.args[1]))
@@ -1,1315 +1,1340 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
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 import os, mimetypes, re, cgi, copy
8 from __future__ import absolute_import
9 import webutil
9
10 from mercurial import error, encoding, archival, templater, templatefilters
10 import cgi
11 from mercurial.node import short, hex
11 import copy
12 from mercurial import util
12 import mimetypes
13 from common import paritygen, staticfile, get_contact, ErrorResponse
13 import os
14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
14 import re
15 from mercurial import graphmod, patch
15
16 from mercurial import scmutil
16 from ..i18n import _
17 from mercurial.i18n import _
17 from ..node import hex, short
18 from mercurial import revset
18
19 from .common import (
20 ErrorResponse,
21 HTTP_FORBIDDEN,
22 HTTP_NOT_FOUND,
23 HTTP_OK,
24 get_contact,
25 paritygen,
26 staticfile,
27 )
28
29 from .. import (
30 archival,
31 encoding,
32 error,
33 graphmod,
34 patch,
35 revset,
36 scmutil,
37 templatefilters,
38 templater,
39 util,
40 )
41
42 from . import (
43 webutil,
44 )
19
45
20 __all__ = []
46 __all__ = []
21 commands = {}
47 commands = {}
22
48
23 class webcommand(object):
49 class webcommand(object):
24 """Decorator used to register a web command handler.
50 """Decorator used to register a web command handler.
25
51
26 The decorator takes as its positional arguments the name/path the
52 The decorator takes as its positional arguments the name/path the
27 command should be accessible under.
53 command should be accessible under.
28
54
29 Usage:
55 Usage:
30
56
31 @webcommand('mycommand')
57 @webcommand('mycommand')
32 def mycommand(web, req, tmpl):
58 def mycommand(web, req, tmpl):
33 pass
59 pass
34 """
60 """
35
61
36 def __init__(self, name):
62 def __init__(self, name):
37 self.name = name
63 self.name = name
38
64
39 def __call__(self, func):
65 def __call__(self, func):
40 __all__.append(self.name)
66 __all__.append(self.name)
41 commands[self.name] = func
67 commands[self.name] = func
42 return func
68 return func
43
69
44 @webcommand('log')
70 @webcommand('log')
45 def log(web, req, tmpl):
71 def log(web, req, tmpl):
46 """
72 """
47 /log[/{revision}[/{path}]]
73 /log[/{revision}[/{path}]]
48 --------------------------
74 --------------------------
49
75
50 Show repository or file history.
76 Show repository or file history.
51
77
52 For URLs of the form ``/log/{revision}``, a list of changesets starting at
78 For URLs of the form ``/log/{revision}``, a list of changesets starting at
53 the specified changeset identifier is shown. If ``{revision}`` is not
79 the specified changeset identifier is shown. If ``{revision}`` is not
54 defined, the default is ``tip``. This form is equivalent to the
80 defined, the default is ``tip``. This form is equivalent to the
55 ``changelog`` handler.
81 ``changelog`` handler.
56
82
57 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
83 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
58 file will be shown. This form is equivalent to the ``filelog`` handler.
84 file will be shown. This form is equivalent to the ``filelog`` handler.
59 """
85 """
60
86
61 if 'file' in req.form and req.form['file'][0]:
87 if 'file' in req.form and req.form['file'][0]:
62 return filelog(web, req, tmpl)
88 return filelog(web, req, tmpl)
63 else:
89 else:
64 return changelog(web, req, tmpl)
90 return changelog(web, req, tmpl)
65
91
66 @webcommand('rawfile')
92 @webcommand('rawfile')
67 def rawfile(web, req, tmpl):
93 def rawfile(web, req, tmpl):
68 guessmime = web.configbool('web', 'guessmime', False)
94 guessmime = web.configbool('web', 'guessmime', False)
69
95
70 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
96 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
71 if not path:
97 if not path:
72 content = manifest(web, req, tmpl)
98 content = manifest(web, req, tmpl)
73 req.respond(HTTP_OK, web.ctype)
99 req.respond(HTTP_OK, web.ctype)
74 return content
100 return content
75
101
76 try:
102 try:
77 fctx = webutil.filectx(web.repo, req)
103 fctx = webutil.filectx(web.repo, req)
78 except error.LookupError as inst:
104 except error.LookupError as inst:
79 try:
105 try:
80 content = manifest(web, req, tmpl)
106 content = manifest(web, req, tmpl)
81 req.respond(HTTP_OK, web.ctype)
107 req.respond(HTTP_OK, web.ctype)
82 return content
108 return content
83 except ErrorResponse:
109 except ErrorResponse:
84 raise inst
110 raise inst
85
111
86 path = fctx.path()
112 path = fctx.path()
87 text = fctx.data()
113 text = fctx.data()
88 mt = 'application/binary'
114 mt = 'application/binary'
89 if guessmime:
115 if guessmime:
90 mt = mimetypes.guess_type(path)[0]
116 mt = mimetypes.guess_type(path)[0]
91 if mt is None:
117 if mt is None:
92 if util.binary(text):
118 if util.binary(text):
93 mt = 'application/binary'
119 mt = 'application/binary'
94 else:
120 else:
95 mt = 'text/plain'
121 mt = 'text/plain'
96 if mt.startswith('text/'):
122 if mt.startswith('text/'):
97 mt += '; charset="%s"' % encoding.encoding
123 mt += '; charset="%s"' % encoding.encoding
98
124
99 req.respond(HTTP_OK, mt, path, body=text)
125 req.respond(HTTP_OK, mt, path, body=text)
100 return []
126 return []
101
127
102 def _filerevision(web, req, tmpl, fctx):
128 def _filerevision(web, req, tmpl, fctx):
103 f = fctx.path()
129 f = fctx.path()
104 text = fctx.data()
130 text = fctx.data()
105 parity = paritygen(web.stripecount)
131 parity = paritygen(web.stripecount)
106
132
107 if util.binary(text):
133 if util.binary(text):
108 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
134 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
109 text = '(binary:%s)' % mt
135 text = '(binary:%s)' % mt
110
136
111 def lines():
137 def lines():
112 for lineno, t in enumerate(text.splitlines(True)):
138 for lineno, t in enumerate(text.splitlines(True)):
113 yield {"line": t,
139 yield {"line": t,
114 "lineid": "l%d" % (lineno + 1),
140 "lineid": "l%d" % (lineno + 1),
115 "linenumber": "% 6d" % (lineno + 1),
141 "linenumber": "% 6d" % (lineno + 1),
116 "parity": parity.next()}
142 "parity": parity.next()}
117
143
118 return tmpl("filerevision",
144 return tmpl("filerevision",
119 file=f,
145 file=f,
120 path=webutil.up(f),
146 path=webutil.up(f),
121 text=lines(),
147 text=lines(),
122 rev=fctx.rev(),
148 rev=fctx.rev(),
123 symrev=webutil.symrevorshortnode(req, fctx),
149 symrev=webutil.symrevorshortnode(req, fctx),
124 node=fctx.hex(),
150 node=fctx.hex(),
125 author=fctx.user(),
151 author=fctx.user(),
126 date=fctx.date(),
152 date=fctx.date(),
127 desc=fctx.description(),
153 desc=fctx.description(),
128 extra=fctx.extra(),
154 extra=fctx.extra(),
129 branch=webutil.nodebranchnodefault(fctx),
155 branch=webutil.nodebranchnodefault(fctx),
130 parent=webutil.parents(fctx),
156 parent=webutil.parents(fctx),
131 child=webutil.children(fctx),
157 child=webutil.children(fctx),
132 rename=webutil.renamelink(fctx),
158 rename=webutil.renamelink(fctx),
133 tags=webutil.nodetagsdict(web.repo, fctx.node()),
159 tags=webutil.nodetagsdict(web.repo, fctx.node()),
134 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
160 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
135 permissions=fctx.manifest().flags(f))
161 permissions=fctx.manifest().flags(f))
136
162
137 @webcommand('file')
163 @webcommand('file')
138 def file(web, req, tmpl):
164 def file(web, req, tmpl):
139 """
165 """
140 /file/{revision}[/{path}]
166 /file/{revision}[/{path}]
141 -------------------------
167 -------------------------
142
168
143 Show information about a directory or file in the repository.
169 Show information about a directory or file in the repository.
144
170
145 Info about the ``path`` given as a URL parameter will be rendered.
171 Info about the ``path`` given as a URL parameter will be rendered.
146
172
147 If ``path`` is a directory, information about the entries in that
173 If ``path`` is a directory, information about the entries in that
148 directory will be rendered. This form is equivalent to the ``manifest``
174 directory will be rendered. This form is equivalent to the ``manifest``
149 handler.
175 handler.
150
176
151 If ``path`` is a file, information about that file will be shown via
177 If ``path`` is a file, information about that file will be shown via
152 the ``filerevision`` template.
178 the ``filerevision`` template.
153
179
154 If ``path`` is not defined, information about the root directory will
180 If ``path`` is not defined, information about the root directory will
155 be rendered.
181 be rendered.
156 """
182 """
157 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
183 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
158 if not path:
184 if not path:
159 return manifest(web, req, tmpl)
185 return manifest(web, req, tmpl)
160 try:
186 try:
161 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
187 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
162 except error.LookupError as inst:
188 except error.LookupError as inst:
163 try:
189 try:
164 return manifest(web, req, tmpl)
190 return manifest(web, req, tmpl)
165 except ErrorResponse:
191 except ErrorResponse:
166 raise inst
192 raise inst
167
193
168 def _search(web, req, tmpl):
194 def _search(web, req, tmpl):
169 MODE_REVISION = 'rev'
195 MODE_REVISION = 'rev'
170 MODE_KEYWORD = 'keyword'
196 MODE_KEYWORD = 'keyword'
171 MODE_REVSET = 'revset'
197 MODE_REVSET = 'revset'
172
198
173 def revsearch(ctx):
199 def revsearch(ctx):
174 yield ctx
200 yield ctx
175
201
176 def keywordsearch(query):
202 def keywordsearch(query):
177 lower = encoding.lower
203 lower = encoding.lower
178 qw = lower(query).split()
204 qw = lower(query).split()
179
205
180 def revgen():
206 def revgen():
181 cl = web.repo.changelog
207 cl = web.repo.changelog
182 for i in xrange(len(web.repo) - 1, 0, -100):
208 for i in xrange(len(web.repo) - 1, 0, -100):
183 l = []
209 l = []
184 for j in cl.revs(max(0, i - 99), i):
210 for j in cl.revs(max(0, i - 99), i):
185 ctx = web.repo[j]
211 ctx = web.repo[j]
186 l.append(ctx)
212 l.append(ctx)
187 l.reverse()
213 l.reverse()
188 for e in l:
214 for e in l:
189 yield e
215 yield e
190
216
191 for ctx in revgen():
217 for ctx in revgen():
192 miss = 0
218 miss = 0
193 for q in qw:
219 for q in qw:
194 if not (q in lower(ctx.user()) or
220 if not (q in lower(ctx.user()) or
195 q in lower(ctx.description()) or
221 q in lower(ctx.description()) or
196 q in lower(" ".join(ctx.files()))):
222 q in lower(" ".join(ctx.files()))):
197 miss = 1
223 miss = 1
198 break
224 break
199 if miss:
225 if miss:
200 continue
226 continue
201
227
202 yield ctx
228 yield ctx
203
229
204 def revsetsearch(revs):
230 def revsetsearch(revs):
205 for r in revs:
231 for r in revs:
206 yield web.repo[r]
232 yield web.repo[r]
207
233
208 searchfuncs = {
234 searchfuncs = {
209 MODE_REVISION: (revsearch, 'exact revision search'),
235 MODE_REVISION: (revsearch, 'exact revision search'),
210 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
236 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
211 MODE_REVSET: (revsetsearch, 'revset expression search'),
237 MODE_REVSET: (revsetsearch, 'revset expression search'),
212 }
238 }
213
239
214 def getsearchmode(query):
240 def getsearchmode(query):
215 try:
241 try:
216 ctx = web.repo[query]
242 ctx = web.repo[query]
217 except (error.RepoError, error.LookupError):
243 except (error.RepoError, error.LookupError):
218 # query is not an exact revision pointer, need to
244 # query is not an exact revision pointer, need to
219 # decide if it's a revset expression or keywords
245 # decide if it's a revset expression or keywords
220 pass
246 pass
221 else:
247 else:
222 return MODE_REVISION, ctx
248 return MODE_REVISION, ctx
223
249
224 revdef = 'reverse(%s)' % query
250 revdef = 'reverse(%s)' % query
225 try:
251 try:
226 tree = revset.parse(revdef)
252 tree = revset.parse(revdef)
227 except error.ParseError:
253 except error.ParseError:
228 # can't parse to a revset tree
254 # can't parse to a revset tree
229 return MODE_KEYWORD, query
255 return MODE_KEYWORD, query
230
256
231 if revset.depth(tree) <= 2:
257 if revset.depth(tree) <= 2:
232 # no revset syntax used
258 # no revset syntax used
233 return MODE_KEYWORD, query
259 return MODE_KEYWORD, query
234
260
235 if any((token, (value or '')[:3]) == ('string', 're:')
261 if any((token, (value or '')[:3]) == ('string', 're:')
236 for token, value, pos in revset.tokenize(revdef)):
262 for token, value, pos in revset.tokenize(revdef)):
237 return MODE_KEYWORD, query
263 return MODE_KEYWORD, query
238
264
239 funcsused = revset.funcsused(tree)
265 funcsused = revset.funcsused(tree)
240 if not funcsused.issubset(revset.safesymbols):
266 if not funcsused.issubset(revset.safesymbols):
241 return MODE_KEYWORD, query
267 return MODE_KEYWORD, query
242
268
243 mfunc = revset.match(web.repo.ui, revdef)
269 mfunc = revset.match(web.repo.ui, revdef)
244 try:
270 try:
245 revs = mfunc(web.repo)
271 revs = mfunc(web.repo)
246 return MODE_REVSET, revs
272 return MODE_REVSET, revs
247 # ParseError: wrongly placed tokens, wrongs arguments, etc
273 # ParseError: wrongly placed tokens, wrongs arguments, etc
248 # RepoLookupError: no such revision, e.g. in 'revision:'
274 # RepoLookupError: no such revision, e.g. in 'revision:'
249 # Abort: bookmark/tag not exists
275 # Abort: bookmark/tag not exists
250 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
276 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
251 except (error.ParseError, error.RepoLookupError, error.Abort,
277 except (error.ParseError, error.RepoLookupError, error.Abort,
252 LookupError):
278 LookupError):
253 return MODE_KEYWORD, query
279 return MODE_KEYWORD, query
254
280
255 def changelist(**map):
281 def changelist(**map):
256 count = 0
282 count = 0
257
283
258 for ctx in searchfunc[0](funcarg):
284 for ctx in searchfunc[0](funcarg):
259 count += 1
285 count += 1
260 n = ctx.node()
286 n = ctx.node()
261 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
287 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
262 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
288 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
263
289
264 yield tmpl('searchentry',
290 yield tmpl('searchentry',
265 parity=parity.next(),
291 parity=parity.next(),
266 author=ctx.user(),
292 author=ctx.user(),
267 parent=lambda **x: webutil.parents(ctx),
293 parent=lambda **x: webutil.parents(ctx),
268 child=lambda **x: webutil.children(ctx),
294 child=lambda **x: webutil.children(ctx),
269 changelogtag=showtags,
295 changelogtag=showtags,
270 desc=ctx.description(),
296 desc=ctx.description(),
271 extra=ctx.extra(),
297 extra=ctx.extra(),
272 date=ctx.date(),
298 date=ctx.date(),
273 files=files,
299 files=files,
274 rev=ctx.rev(),
300 rev=ctx.rev(),
275 node=hex(n),
301 node=hex(n),
276 tags=webutil.nodetagsdict(web.repo, n),
302 tags=webutil.nodetagsdict(web.repo, n),
277 bookmarks=webutil.nodebookmarksdict(web.repo, n),
303 bookmarks=webutil.nodebookmarksdict(web.repo, n),
278 inbranch=webutil.nodeinbranch(web.repo, ctx),
304 inbranch=webutil.nodeinbranch(web.repo, ctx),
279 branches=webutil.nodebranchdict(web.repo, ctx))
305 branches=webutil.nodebranchdict(web.repo, ctx))
280
306
281 if count >= revcount:
307 if count >= revcount:
282 break
308 break
283
309
284 query = req.form['rev'][0]
310 query = req.form['rev'][0]
285 revcount = web.maxchanges
311 revcount = web.maxchanges
286 if 'revcount' in req.form:
312 if 'revcount' in req.form:
287 try:
313 try:
288 revcount = int(req.form.get('revcount', [revcount])[0])
314 revcount = int(req.form.get('revcount', [revcount])[0])
289 revcount = max(revcount, 1)
315 revcount = max(revcount, 1)
290 tmpl.defaults['sessionvars']['revcount'] = revcount
316 tmpl.defaults['sessionvars']['revcount'] = revcount
291 except ValueError:
317 except ValueError:
292 pass
318 pass
293
319
294 lessvars = copy.copy(tmpl.defaults['sessionvars'])
320 lessvars = copy.copy(tmpl.defaults['sessionvars'])
295 lessvars['revcount'] = max(revcount / 2, 1)
321 lessvars['revcount'] = max(revcount / 2, 1)
296 lessvars['rev'] = query
322 lessvars['rev'] = query
297 morevars = copy.copy(tmpl.defaults['sessionvars'])
323 morevars = copy.copy(tmpl.defaults['sessionvars'])
298 morevars['revcount'] = revcount * 2
324 morevars['revcount'] = revcount * 2
299 morevars['rev'] = query
325 morevars['rev'] = query
300
326
301 mode, funcarg = getsearchmode(query)
327 mode, funcarg = getsearchmode(query)
302
328
303 if 'forcekw' in req.form:
329 if 'forcekw' in req.form:
304 showforcekw = ''
330 showforcekw = ''
305 showunforcekw = searchfuncs[mode][1]
331 showunforcekw = searchfuncs[mode][1]
306 mode = MODE_KEYWORD
332 mode = MODE_KEYWORD
307 funcarg = query
333 funcarg = query
308 else:
334 else:
309 if mode != MODE_KEYWORD:
335 if mode != MODE_KEYWORD:
310 showforcekw = searchfuncs[MODE_KEYWORD][1]
336 showforcekw = searchfuncs[MODE_KEYWORD][1]
311 else:
337 else:
312 showforcekw = ''
338 showforcekw = ''
313 showunforcekw = ''
339 showunforcekw = ''
314
340
315 searchfunc = searchfuncs[mode]
341 searchfunc = searchfuncs[mode]
316
342
317 tip = web.repo['tip']
343 tip = web.repo['tip']
318 parity = paritygen(web.stripecount)
344 parity = paritygen(web.stripecount)
319
345
320 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
346 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
321 entries=changelist, archives=web.archivelist("tip"),
347 entries=changelist, archives=web.archivelist("tip"),
322 morevars=morevars, lessvars=lessvars,
348 morevars=morevars, lessvars=lessvars,
323 modedesc=searchfunc[1],
349 modedesc=searchfunc[1],
324 showforcekw=showforcekw, showunforcekw=showunforcekw)
350 showforcekw=showforcekw, showunforcekw=showunforcekw)
325
351
326 @webcommand('changelog')
352 @webcommand('changelog')
327 def changelog(web, req, tmpl, shortlog=False):
353 def changelog(web, req, tmpl, shortlog=False):
328 """
354 """
329 /changelog[/{revision}]
355 /changelog[/{revision}]
330 -----------------------
356 -----------------------
331
357
332 Show information about multiple changesets.
358 Show information about multiple changesets.
333
359
334 If the optional ``revision`` URL argument is absent, information about
360 If the optional ``revision`` URL argument is absent, information about
335 all changesets starting at ``tip`` will be rendered. If the ``revision``
361 all changesets starting at ``tip`` will be rendered. If the ``revision``
336 argument is present, changesets will be shown starting from the specified
362 argument is present, changesets will be shown starting from the specified
337 revision.
363 revision.
338
364
339 If ``revision`` is absent, the ``rev`` query string argument may be
365 If ``revision`` is absent, the ``rev`` query string argument may be
340 defined. This will perform a search for changesets.
366 defined. This will perform a search for changesets.
341
367
342 The argument for ``rev`` can be a single revision, a revision set,
368 The argument for ``rev`` can be a single revision, a revision set,
343 or a literal keyword to search for in changeset data (equivalent to
369 or a literal keyword to search for in changeset data (equivalent to
344 :hg:`log -k`).
370 :hg:`log -k`).
345
371
346 The ``revcount`` query string argument defines the maximum numbers of
372 The ``revcount`` query string argument defines the maximum numbers of
347 changesets to render.
373 changesets to render.
348
374
349 For non-searches, the ``changelog`` template will be rendered.
375 For non-searches, the ``changelog`` template will be rendered.
350 """
376 """
351
377
352 query = ''
378 query = ''
353 if 'node' in req.form:
379 if 'node' in req.form:
354 ctx = webutil.changectx(web.repo, req)
380 ctx = webutil.changectx(web.repo, req)
355 symrev = webutil.symrevorshortnode(req, ctx)
381 symrev = webutil.symrevorshortnode(req, ctx)
356 elif 'rev' in req.form:
382 elif 'rev' in req.form:
357 return _search(web, req, tmpl)
383 return _search(web, req, tmpl)
358 else:
384 else:
359 ctx = web.repo['tip']
385 ctx = web.repo['tip']
360 symrev = 'tip'
386 symrev = 'tip'
361
387
362 def changelist():
388 def changelist():
363 revs = []
389 revs = []
364 if pos != -1:
390 if pos != -1:
365 revs = web.repo.changelog.revs(pos, 0)
391 revs = web.repo.changelog.revs(pos, 0)
366 curcount = 0
392 curcount = 0
367 for rev in revs:
393 for rev in revs:
368 curcount += 1
394 curcount += 1
369 if curcount > revcount + 1:
395 if curcount > revcount + 1:
370 break
396 break
371
397
372 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
398 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
373 entry['parity'] = parity.next()
399 entry['parity'] = parity.next()
374 yield entry
400 yield entry
375
401
376 if shortlog:
402 if shortlog:
377 revcount = web.maxshortchanges
403 revcount = web.maxshortchanges
378 else:
404 else:
379 revcount = web.maxchanges
405 revcount = web.maxchanges
380
406
381 if 'revcount' in req.form:
407 if 'revcount' in req.form:
382 try:
408 try:
383 revcount = int(req.form.get('revcount', [revcount])[0])
409 revcount = int(req.form.get('revcount', [revcount])[0])
384 revcount = max(revcount, 1)
410 revcount = max(revcount, 1)
385 tmpl.defaults['sessionvars']['revcount'] = revcount
411 tmpl.defaults['sessionvars']['revcount'] = revcount
386 except ValueError:
412 except ValueError:
387 pass
413 pass
388
414
389 lessvars = copy.copy(tmpl.defaults['sessionvars'])
415 lessvars = copy.copy(tmpl.defaults['sessionvars'])
390 lessvars['revcount'] = max(revcount / 2, 1)
416 lessvars['revcount'] = max(revcount / 2, 1)
391 morevars = copy.copy(tmpl.defaults['sessionvars'])
417 morevars = copy.copy(tmpl.defaults['sessionvars'])
392 morevars['revcount'] = revcount * 2
418 morevars['revcount'] = revcount * 2
393
419
394 count = len(web.repo)
420 count = len(web.repo)
395 pos = ctx.rev()
421 pos = ctx.rev()
396 parity = paritygen(web.stripecount)
422 parity = paritygen(web.stripecount)
397
423
398 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
424 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
399
425
400 entries = list(changelist())
426 entries = list(changelist())
401 latestentry = entries[:1]
427 latestentry = entries[:1]
402 if len(entries) > revcount:
428 if len(entries) > revcount:
403 nextentry = entries[-1:]
429 nextentry = entries[-1:]
404 entries = entries[:-1]
430 entries = entries[:-1]
405 else:
431 else:
406 nextentry = []
432 nextentry = []
407
433
408 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
434 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
409 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
435 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
410 entries=entries,
436 entries=entries,
411 latestentry=latestentry, nextentry=nextentry,
437 latestentry=latestentry, nextentry=nextentry,
412 archives=web.archivelist("tip"), revcount=revcount,
438 archives=web.archivelist("tip"), revcount=revcount,
413 morevars=morevars, lessvars=lessvars, query=query)
439 morevars=morevars, lessvars=lessvars, query=query)
414
440
415 @webcommand('shortlog')
441 @webcommand('shortlog')
416 def shortlog(web, req, tmpl):
442 def shortlog(web, req, tmpl):
417 """
443 """
418 /shortlog
444 /shortlog
419 ---------
445 ---------
420
446
421 Show basic information about a set of changesets.
447 Show basic information about a set of changesets.
422
448
423 This accepts the same parameters as the ``changelog`` handler. The only
449 This accepts the same parameters as the ``changelog`` handler. The only
424 difference is the ``shortlog`` template will be rendered instead of the
450 difference is the ``shortlog`` template will be rendered instead of the
425 ``changelog`` template.
451 ``changelog`` template.
426 """
452 """
427 return changelog(web, req, tmpl, shortlog=True)
453 return changelog(web, req, tmpl, shortlog=True)
428
454
429 @webcommand('changeset')
455 @webcommand('changeset')
430 def changeset(web, req, tmpl):
456 def changeset(web, req, tmpl):
431 """
457 """
432 /changeset[/{revision}]
458 /changeset[/{revision}]
433 -----------------------
459 -----------------------
434
460
435 Show information about a single changeset.
461 Show information about a single changeset.
436
462
437 A URL path argument is the changeset identifier to show. See ``hg help
463 A URL path argument is the changeset identifier to show. See ``hg help
438 revisions`` for possible values. If not defined, the ``tip`` changeset
464 revisions`` for possible values. If not defined, the ``tip`` changeset
439 will be shown.
465 will be shown.
440
466
441 The ``changeset`` template is rendered. Contents of the ``changesettag``,
467 The ``changeset`` template is rendered. Contents of the ``changesettag``,
442 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
468 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
443 templates related to diffs may all be used to produce the output.
469 templates related to diffs may all be used to produce the output.
444 """
470 """
445 ctx = webutil.changectx(web.repo, req)
471 ctx = webutil.changectx(web.repo, req)
446
472
447 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
473 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
448
474
449 rev = webcommand('rev')(changeset)
475 rev = webcommand('rev')(changeset)
450
476
451 def decodepath(path):
477 def decodepath(path):
452 """Hook for mapping a path in the repository to a path in the
478 """Hook for mapping a path in the repository to a path in the
453 working copy.
479 working copy.
454
480
455 Extensions (e.g., largefiles) can override this to remap files in
481 Extensions (e.g., largefiles) can override this to remap files in
456 the virtual file system presented by the manifest command below."""
482 the virtual file system presented by the manifest command below."""
457 return path
483 return path
458
484
459 @webcommand('manifest')
485 @webcommand('manifest')
460 def manifest(web, req, tmpl):
486 def manifest(web, req, tmpl):
461 """
487 """
462 /manifest[/{revision}[/{path}]]
488 /manifest[/{revision}[/{path}]]
463 -------------------------------
489 -------------------------------
464
490
465 Show information about a directory.
491 Show information about a directory.
466
492
467 If the URL path arguments are omitted, information about the root
493 If the URL path arguments are omitted, information about the root
468 directory for the ``tip`` changeset will be shown.
494 directory for the ``tip`` changeset will be shown.
469
495
470 Because this handler can only show information for directories, it
496 Because this handler can only show information for directories, it
471 is recommended to use the ``file`` handler instead, as it can handle both
497 is recommended to use the ``file`` handler instead, as it can handle both
472 directories and files.
498 directories and files.
473
499
474 The ``manifest`` template will be rendered for this handler.
500 The ``manifest`` template will be rendered for this handler.
475 """
501 """
476 if 'node' in req.form:
502 if 'node' in req.form:
477 ctx = webutil.changectx(web.repo, req)
503 ctx = webutil.changectx(web.repo, req)
478 symrev = webutil.symrevorshortnode(req, ctx)
504 symrev = webutil.symrevorshortnode(req, ctx)
479 else:
505 else:
480 ctx = web.repo['tip']
506 ctx = web.repo['tip']
481 symrev = 'tip'
507 symrev = 'tip'
482 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
508 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
483 mf = ctx.manifest()
509 mf = ctx.manifest()
484 node = ctx.node()
510 node = ctx.node()
485
511
486 files = {}
512 files = {}
487 dirs = {}
513 dirs = {}
488 parity = paritygen(web.stripecount)
514 parity = paritygen(web.stripecount)
489
515
490 if path and path[-1] != "/":
516 if path and path[-1] != "/":
491 path += "/"
517 path += "/"
492 l = len(path)
518 l = len(path)
493 abspath = "/" + path
519 abspath = "/" + path
494
520
495 for full, n in mf.iteritems():
521 for full, n in mf.iteritems():
496 # the virtual path (working copy path) used for the full
522 # the virtual path (working copy path) used for the full
497 # (repository) path
523 # (repository) path
498 f = decodepath(full)
524 f = decodepath(full)
499
525
500 if f[:l] != path:
526 if f[:l] != path:
501 continue
527 continue
502 remain = f[l:]
528 remain = f[l:]
503 elements = remain.split('/')
529 elements = remain.split('/')
504 if len(elements) == 1:
530 if len(elements) == 1:
505 files[remain] = full
531 files[remain] = full
506 else:
532 else:
507 h = dirs # need to retain ref to dirs (root)
533 h = dirs # need to retain ref to dirs (root)
508 for elem in elements[0:-1]:
534 for elem in elements[0:-1]:
509 if elem not in h:
535 if elem not in h:
510 h[elem] = {}
536 h[elem] = {}
511 h = h[elem]
537 h = h[elem]
512 if len(h) > 1:
538 if len(h) > 1:
513 break
539 break
514 h[None] = None # denotes files present
540 h[None] = None # denotes files present
515
541
516 if mf and not files and not dirs:
542 if mf and not files and not dirs:
517 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
543 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
518
544
519 def filelist(**map):
545 def filelist(**map):
520 for f in sorted(files):
546 for f in sorted(files):
521 full = files[f]
547 full = files[f]
522
548
523 fctx = ctx.filectx(full)
549 fctx = ctx.filectx(full)
524 yield {"file": full,
550 yield {"file": full,
525 "parity": parity.next(),
551 "parity": parity.next(),
526 "basename": f,
552 "basename": f,
527 "date": fctx.date(),
553 "date": fctx.date(),
528 "size": fctx.size(),
554 "size": fctx.size(),
529 "permissions": mf.flags(full)}
555 "permissions": mf.flags(full)}
530
556
531 def dirlist(**map):
557 def dirlist(**map):
532 for d in sorted(dirs):
558 for d in sorted(dirs):
533
559
534 emptydirs = []
560 emptydirs = []
535 h = dirs[d]
561 h = dirs[d]
536 while isinstance(h, dict) and len(h) == 1:
562 while isinstance(h, dict) and len(h) == 1:
537 k, v = h.items()[0]
563 k, v = h.items()[0]
538 if v:
564 if v:
539 emptydirs.append(k)
565 emptydirs.append(k)
540 h = v
566 h = v
541
567
542 path = "%s%s" % (abspath, d)
568 path = "%s%s" % (abspath, d)
543 yield {"parity": parity.next(),
569 yield {"parity": parity.next(),
544 "path": path,
570 "path": path,
545 "emptydirs": "/".join(emptydirs),
571 "emptydirs": "/".join(emptydirs),
546 "basename": d}
572 "basename": d}
547
573
548 return tmpl("manifest",
574 return tmpl("manifest",
549 rev=ctx.rev(),
575 rev=ctx.rev(),
550 symrev=symrev,
576 symrev=symrev,
551 node=hex(node),
577 node=hex(node),
552 path=abspath,
578 path=abspath,
553 up=webutil.up(abspath),
579 up=webutil.up(abspath),
554 upparity=parity.next(),
580 upparity=parity.next(),
555 fentries=filelist,
581 fentries=filelist,
556 dentries=dirlist,
582 dentries=dirlist,
557 archives=web.archivelist(hex(node)),
583 archives=web.archivelist(hex(node)),
558 tags=webutil.nodetagsdict(web.repo, node),
584 tags=webutil.nodetagsdict(web.repo, node),
559 bookmarks=webutil.nodebookmarksdict(web.repo, node),
585 bookmarks=webutil.nodebookmarksdict(web.repo, node),
560 branch=webutil.nodebranchnodefault(ctx),
586 branch=webutil.nodebranchnodefault(ctx),
561 inbranch=webutil.nodeinbranch(web.repo, ctx),
587 inbranch=webutil.nodeinbranch(web.repo, ctx),
562 branches=webutil.nodebranchdict(web.repo, ctx))
588 branches=webutil.nodebranchdict(web.repo, ctx))
563
589
564 @webcommand('tags')
590 @webcommand('tags')
565 def tags(web, req, tmpl):
591 def tags(web, req, tmpl):
566 """
592 """
567 /tags
593 /tags
568 -----
594 -----
569
595
570 Show information about tags.
596 Show information about tags.
571
597
572 No arguments are accepted.
598 No arguments are accepted.
573
599
574 The ``tags`` template is rendered.
600 The ``tags`` template is rendered.
575 """
601 """
576 i = list(reversed(web.repo.tagslist()))
602 i = list(reversed(web.repo.tagslist()))
577 parity = paritygen(web.stripecount)
603 parity = paritygen(web.stripecount)
578
604
579 def entries(notip, latestonly, **map):
605 def entries(notip, latestonly, **map):
580 t = i
606 t = i
581 if notip:
607 if notip:
582 t = [(k, n) for k, n in i if k != "tip"]
608 t = [(k, n) for k, n in i if k != "tip"]
583 if latestonly:
609 if latestonly:
584 t = t[:1]
610 t = t[:1]
585 for k, n in t:
611 for k, n in t:
586 yield {"parity": parity.next(),
612 yield {"parity": parity.next(),
587 "tag": k,
613 "tag": k,
588 "date": web.repo[n].date(),
614 "date": web.repo[n].date(),
589 "node": hex(n)}
615 "node": hex(n)}
590
616
591 return tmpl("tags",
617 return tmpl("tags",
592 node=hex(web.repo.changelog.tip()),
618 node=hex(web.repo.changelog.tip()),
593 entries=lambda **x: entries(False, False, **x),
619 entries=lambda **x: entries(False, False, **x),
594 entriesnotip=lambda **x: entries(True, False, **x),
620 entriesnotip=lambda **x: entries(True, False, **x),
595 latestentry=lambda **x: entries(True, True, **x))
621 latestentry=lambda **x: entries(True, True, **x))
596
622
597 @webcommand('bookmarks')
623 @webcommand('bookmarks')
598 def bookmarks(web, req, tmpl):
624 def bookmarks(web, req, tmpl):
599 """
625 """
600 /bookmarks
626 /bookmarks
601 ----------
627 ----------
602
628
603 Show information about bookmarks.
629 Show information about bookmarks.
604
630
605 No arguments are accepted.
631 No arguments are accepted.
606
632
607 The ``bookmarks`` template is rendered.
633 The ``bookmarks`` template is rendered.
608 """
634 """
609 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
635 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
610 parity = paritygen(web.stripecount)
636 parity = paritygen(web.stripecount)
611
637
612 def entries(latestonly, **map):
638 def entries(latestonly, **map):
613 if latestonly:
639 if latestonly:
614 t = [min(i)]
640 t = [min(i)]
615 else:
641 else:
616 t = sorted(i)
642 t = sorted(i)
617 for k, n in t:
643 for k, n in t:
618 yield {"parity": parity.next(),
644 yield {"parity": parity.next(),
619 "bookmark": k,
645 "bookmark": k,
620 "date": web.repo[n].date(),
646 "date": web.repo[n].date(),
621 "node": hex(n)}
647 "node": hex(n)}
622
648
623 return tmpl("bookmarks",
649 return tmpl("bookmarks",
624 node=hex(web.repo.changelog.tip()),
650 node=hex(web.repo.changelog.tip()),
625 entries=lambda **x: entries(latestonly=False, **x),
651 entries=lambda **x: entries(latestonly=False, **x),
626 latestentry=lambda **x: entries(latestonly=True, **x))
652 latestentry=lambda **x: entries(latestonly=True, **x))
627
653
628 @webcommand('branches')
654 @webcommand('branches')
629 def branches(web, req, tmpl):
655 def branches(web, req, tmpl):
630 """
656 """
631 /branches
657 /branches
632 ---------
658 ---------
633
659
634 Show information about branches.
660 Show information about branches.
635
661
636 All known branches are contained in the output, even closed branches.
662 All known branches are contained in the output, even closed branches.
637
663
638 No arguments are accepted.
664 No arguments are accepted.
639
665
640 The ``branches`` template is rendered.
666 The ``branches`` template is rendered.
641 """
667 """
642 entries = webutil.branchentries(web.repo, web.stripecount)
668 entries = webutil.branchentries(web.repo, web.stripecount)
643 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
669 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
644 return tmpl('branches', node=hex(web.repo.changelog.tip()),
670 return tmpl('branches', node=hex(web.repo.changelog.tip()),
645 entries=entries, latestentry=latestentry)
671 entries=entries, latestentry=latestentry)
646
672
647 @webcommand('summary')
673 @webcommand('summary')
648 def summary(web, req, tmpl):
674 def summary(web, req, tmpl):
649 """
675 """
650 /summary
676 /summary
651 --------
677 --------
652
678
653 Show a summary of repository state.
679 Show a summary of repository state.
654
680
655 Information about the latest changesets, bookmarks, tags, and branches
681 Information about the latest changesets, bookmarks, tags, and branches
656 is captured by this handler.
682 is captured by this handler.
657
683
658 The ``summary`` template is rendered.
684 The ``summary`` template is rendered.
659 """
685 """
660 i = reversed(web.repo.tagslist())
686 i = reversed(web.repo.tagslist())
661
687
662 def tagentries(**map):
688 def tagentries(**map):
663 parity = paritygen(web.stripecount)
689 parity = paritygen(web.stripecount)
664 count = 0
690 count = 0
665 for k, n in i:
691 for k, n in i:
666 if k == "tip": # skip tip
692 if k == "tip": # skip tip
667 continue
693 continue
668
694
669 count += 1
695 count += 1
670 if count > 10: # limit to 10 tags
696 if count > 10: # limit to 10 tags
671 break
697 break
672
698
673 yield tmpl("tagentry",
699 yield tmpl("tagentry",
674 parity=parity.next(),
700 parity=parity.next(),
675 tag=k,
701 tag=k,
676 node=hex(n),
702 node=hex(n),
677 date=web.repo[n].date())
703 date=web.repo[n].date())
678
704
679 def bookmarks(**map):
705 def bookmarks(**map):
680 parity = paritygen(web.stripecount)
706 parity = paritygen(web.stripecount)
681 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
707 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
682 for k, n in sorted(marks)[:10]: # limit to 10 bookmarks
708 for k, n in sorted(marks)[:10]: # limit to 10 bookmarks
683 yield {'parity': parity.next(),
709 yield {'parity': parity.next(),
684 'bookmark': k,
710 'bookmark': k,
685 'date': web.repo[n].date(),
711 'date': web.repo[n].date(),
686 'node': hex(n)}
712 'node': hex(n)}
687
713
688 def changelist(**map):
714 def changelist(**map):
689 parity = paritygen(web.stripecount, offset=start - end)
715 parity = paritygen(web.stripecount, offset=start - end)
690 l = [] # build a list in forward order for efficiency
716 l = [] # build a list in forward order for efficiency
691 revs = []
717 revs = []
692 if start < end:
718 if start < end:
693 revs = web.repo.changelog.revs(start, end - 1)
719 revs = web.repo.changelog.revs(start, end - 1)
694 for i in revs:
720 for i in revs:
695 ctx = web.repo[i]
721 ctx = web.repo[i]
696 n = ctx.node()
722 n = ctx.node()
697 hn = hex(n)
723 hn = hex(n)
698
724
699 l.append(tmpl(
725 l.append(tmpl(
700 'shortlogentry',
726 'shortlogentry',
701 parity=parity.next(),
727 parity=parity.next(),
702 author=ctx.user(),
728 author=ctx.user(),
703 desc=ctx.description(),
729 desc=ctx.description(),
704 extra=ctx.extra(),
730 extra=ctx.extra(),
705 date=ctx.date(),
731 date=ctx.date(),
706 rev=i,
732 rev=i,
707 node=hn,
733 node=hn,
708 tags=webutil.nodetagsdict(web.repo, n),
734 tags=webutil.nodetagsdict(web.repo, n),
709 bookmarks=webutil.nodebookmarksdict(web.repo, n),
735 bookmarks=webutil.nodebookmarksdict(web.repo, n),
710 inbranch=webutil.nodeinbranch(web.repo, ctx),
736 inbranch=webutil.nodeinbranch(web.repo, ctx),
711 branches=webutil.nodebranchdict(web.repo, ctx)))
737 branches=webutil.nodebranchdict(web.repo, ctx)))
712
738
713 l.reverse()
739 l.reverse()
714 yield l
740 yield l
715
741
716 tip = web.repo['tip']
742 tip = web.repo['tip']
717 count = len(web.repo)
743 count = len(web.repo)
718 start = max(0, count - web.maxchanges)
744 start = max(0, count - web.maxchanges)
719 end = min(count, start + web.maxchanges)
745 end = min(count, start + web.maxchanges)
720
746
721 return tmpl("summary",
747 return tmpl("summary",
722 desc=web.config("web", "description", "unknown"),
748 desc=web.config("web", "description", "unknown"),
723 owner=get_contact(web.config) or "unknown",
749 owner=get_contact(web.config) or "unknown",
724 lastchange=tip.date(),
750 lastchange=tip.date(),
725 tags=tagentries,
751 tags=tagentries,
726 bookmarks=bookmarks,
752 bookmarks=bookmarks,
727 branches=webutil.branchentries(web.repo, web.stripecount, 10),
753 branches=webutil.branchentries(web.repo, web.stripecount, 10),
728 shortlog=changelist,
754 shortlog=changelist,
729 node=tip.hex(),
755 node=tip.hex(),
730 symrev='tip',
756 symrev='tip',
731 archives=web.archivelist("tip"))
757 archives=web.archivelist("tip"))
732
758
733 @webcommand('filediff')
759 @webcommand('filediff')
734 def filediff(web, req, tmpl):
760 def filediff(web, req, tmpl):
735 """
761 """
736 /diff/{revision}/{path}
762 /diff/{revision}/{path}
737 -----------------------
763 -----------------------
738
764
739 Show how a file changed in a particular commit.
765 Show how a file changed in a particular commit.
740
766
741 The ``filediff`` template is rendered.
767 The ``filediff`` template is rendered.
742
768
743 This handler is registered under both the ``/diff`` and ``/filediff``
769 This handler is registered under both the ``/diff`` and ``/filediff``
744 paths. ``/diff`` is used in modern code.
770 paths. ``/diff`` is used in modern code.
745 """
771 """
746 fctx, ctx = None, None
772 fctx, ctx = None, None
747 try:
773 try:
748 fctx = webutil.filectx(web.repo, req)
774 fctx = webutil.filectx(web.repo, req)
749 except LookupError:
775 except LookupError:
750 ctx = webutil.changectx(web.repo, req)
776 ctx = webutil.changectx(web.repo, req)
751 path = webutil.cleanpath(web.repo, req.form['file'][0])
777 path = webutil.cleanpath(web.repo, req.form['file'][0])
752 if path not in ctx.files():
778 if path not in ctx.files():
753 raise
779 raise
754
780
755 if fctx is not None:
781 if fctx is not None:
756 n = fctx.node()
782 n = fctx.node()
757 path = fctx.path()
783 path = fctx.path()
758 ctx = fctx.changectx()
784 ctx = fctx.changectx()
759 else:
785 else:
760 n = ctx.node()
786 n = ctx.node()
761 # path already defined in except clause
787 # path already defined in except clause
762
788
763 parity = paritygen(web.stripecount)
789 parity = paritygen(web.stripecount)
764 style = web.config('web', 'style', 'paper')
790 style = web.config('web', 'style', 'paper')
765 if 'style' in req.form:
791 if 'style' in req.form:
766 style = req.form['style'][0]
792 style = req.form['style'][0]
767
793
768 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
794 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
769 if fctx:
795 if fctx:
770 rename = webutil.renamelink(fctx)
796 rename = webutil.renamelink(fctx)
771 ctx = fctx
797 ctx = fctx
772 else:
798 else:
773 rename = []
799 rename = []
774 ctx = ctx
800 ctx = ctx
775 return tmpl("filediff",
801 return tmpl("filediff",
776 file=path,
802 file=path,
777 node=hex(n),
803 node=hex(n),
778 rev=ctx.rev(),
804 rev=ctx.rev(),
779 symrev=webutil.symrevorshortnode(req, ctx),
805 symrev=webutil.symrevorshortnode(req, ctx),
780 date=ctx.date(),
806 date=ctx.date(),
781 desc=ctx.description(),
807 desc=ctx.description(),
782 extra=ctx.extra(),
808 extra=ctx.extra(),
783 author=ctx.user(),
809 author=ctx.user(),
784 rename=rename,
810 rename=rename,
785 branch=webutil.nodebranchnodefault(ctx),
811 branch=webutil.nodebranchnodefault(ctx),
786 parent=webutil.parents(ctx),
812 parent=webutil.parents(ctx),
787 child=webutil.children(ctx),
813 child=webutil.children(ctx),
788 tags=webutil.nodetagsdict(web.repo, n),
814 tags=webutil.nodetagsdict(web.repo, n),
789 bookmarks=webutil.nodebookmarksdict(web.repo, n),
815 bookmarks=webutil.nodebookmarksdict(web.repo, n),
790 diff=diffs)
816 diff=diffs)
791
817
792 diff = webcommand('diff')(filediff)
818 diff = webcommand('diff')(filediff)
793
819
794 @webcommand('comparison')
820 @webcommand('comparison')
795 def comparison(web, req, tmpl):
821 def comparison(web, req, tmpl):
796 """
822 """
797 /comparison/{revision}/{path}
823 /comparison/{revision}/{path}
798 -----------------------------
824 -----------------------------
799
825
800 Show a comparison between the old and new versions of a file from changes
826 Show a comparison between the old and new versions of a file from changes
801 made on a particular revision.
827 made on a particular revision.
802
828
803 This is similar to the ``diff`` handler. However, this form features
829 This is similar to the ``diff`` handler. However, this form features
804 a split or side-by-side diff rather than a unified diff.
830 a split or side-by-side diff rather than a unified diff.
805
831
806 The ``context`` query string argument can be used to control the lines of
832 The ``context`` query string argument can be used to control the lines of
807 context in the diff.
833 context in the diff.
808
834
809 The ``filecomparison`` template is rendered.
835 The ``filecomparison`` template is rendered.
810 """
836 """
811 ctx = webutil.changectx(web.repo, req)
837 ctx = webutil.changectx(web.repo, req)
812 if 'file' not in req.form:
838 if 'file' not in req.form:
813 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
839 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
814 path = webutil.cleanpath(web.repo, req.form['file'][0])
840 path = webutil.cleanpath(web.repo, req.form['file'][0])
815 rename = path in ctx and webutil.renamelink(ctx[path]) or []
841 rename = path in ctx and webutil.renamelink(ctx[path]) or []
816
842
817 parsecontext = lambda v: v == 'full' and -1 or int(v)
843 parsecontext = lambda v: v == 'full' and -1 or int(v)
818 if 'context' in req.form:
844 if 'context' in req.form:
819 context = parsecontext(req.form['context'][0])
845 context = parsecontext(req.form['context'][0])
820 else:
846 else:
821 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
847 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
822
848
823 def filelines(f):
849 def filelines(f):
824 if util.binary(f.data()):
850 if util.binary(f.data()):
825 mt = mimetypes.guess_type(f.path())[0]
851 mt = mimetypes.guess_type(f.path())[0]
826 if not mt:
852 if not mt:
827 mt = 'application/octet-stream'
853 mt = 'application/octet-stream'
828 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
854 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
829 return f.data().splitlines()
855 return f.data().splitlines()
830
856
831 parent = ctx.p1()
857 parent = ctx.p1()
832 leftrev = parent.rev()
858 leftrev = parent.rev()
833 leftnode = parent.node()
859 leftnode = parent.node()
834 rightrev = ctx.rev()
860 rightrev = ctx.rev()
835 rightnode = ctx.node()
861 rightnode = ctx.node()
836 if path in ctx:
862 if path in ctx:
837 fctx = ctx[path]
863 fctx = ctx[path]
838 rightlines = filelines(fctx)
864 rightlines = filelines(fctx)
839 if path not in parent:
865 if path not in parent:
840 leftlines = ()
866 leftlines = ()
841 else:
867 else:
842 pfctx = parent[path]
868 pfctx = parent[path]
843 leftlines = filelines(pfctx)
869 leftlines = filelines(pfctx)
844 else:
870 else:
845 rightlines = ()
871 rightlines = ()
846 fctx = ctx.parents()[0][path]
872 fctx = ctx.parents()[0][path]
847 leftlines = filelines(fctx)
873 leftlines = filelines(fctx)
848
874
849 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
875 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
850 return tmpl('filecomparison',
876 return tmpl('filecomparison',
851 file=path,
877 file=path,
852 node=hex(ctx.node()),
878 node=hex(ctx.node()),
853 rev=ctx.rev(),
879 rev=ctx.rev(),
854 symrev=webutil.symrevorshortnode(req, ctx),
880 symrev=webutil.symrevorshortnode(req, ctx),
855 date=ctx.date(),
881 date=ctx.date(),
856 desc=ctx.description(),
882 desc=ctx.description(),
857 extra=ctx.extra(),
883 extra=ctx.extra(),
858 author=ctx.user(),
884 author=ctx.user(),
859 rename=rename,
885 rename=rename,
860 branch=webutil.nodebranchnodefault(ctx),
886 branch=webutil.nodebranchnodefault(ctx),
861 parent=webutil.parents(fctx),
887 parent=webutil.parents(fctx),
862 child=webutil.children(fctx),
888 child=webutil.children(fctx),
863 tags=webutil.nodetagsdict(web.repo, ctx.node()),
889 tags=webutil.nodetagsdict(web.repo, ctx.node()),
864 bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()),
890 bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()),
865 leftrev=leftrev,
891 leftrev=leftrev,
866 leftnode=hex(leftnode),
892 leftnode=hex(leftnode),
867 rightrev=rightrev,
893 rightrev=rightrev,
868 rightnode=hex(rightnode),
894 rightnode=hex(rightnode),
869 comparison=comparison)
895 comparison=comparison)
870
896
871 @webcommand('annotate')
897 @webcommand('annotate')
872 def annotate(web, req, tmpl):
898 def annotate(web, req, tmpl):
873 """
899 """
874 /annotate/{revision}/{path}
900 /annotate/{revision}/{path}
875 ---------------------------
901 ---------------------------
876
902
877 Show changeset information for each line in a file.
903 Show changeset information for each line in a file.
878
904
879 The ``fileannotate`` template is rendered.
905 The ``fileannotate`` template is rendered.
880 """
906 """
881 fctx = webutil.filectx(web.repo, req)
907 fctx = webutil.filectx(web.repo, req)
882 f = fctx.path()
908 f = fctx.path()
883 parity = paritygen(web.stripecount)
909 parity = paritygen(web.stripecount)
884 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
910 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
885 section='annotate', whitespace=True)
911 section='annotate', whitespace=True)
886
912
887 def annotate(**map):
913 def annotate(**map):
888 last = None
914 last = None
889 if util.binary(fctx.data()):
915 if util.binary(fctx.data()):
890 mt = (mimetypes.guess_type(fctx.path())[0]
916 mt = (mimetypes.guess_type(fctx.path())[0]
891 or 'application/octet-stream')
917 or 'application/octet-stream')
892 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
918 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
893 '(binary:%s)' % mt)])
919 '(binary:%s)' % mt)])
894 else:
920 else:
895 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
921 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
896 diffopts=diffopts))
922 diffopts=diffopts))
897 for lineno, ((f, targetline), l) in lines:
923 for lineno, ((f, targetline), l) in lines:
898 fnode = f.filenode()
924 fnode = f.filenode()
899
925
900 if last != fnode:
926 if last != fnode:
901 last = fnode
927 last = fnode
902
928
903 yield {"parity": parity.next(),
929 yield {"parity": parity.next(),
904 "node": f.hex(),
930 "node": f.hex(),
905 "rev": f.rev(),
931 "rev": f.rev(),
906 "author": f.user(),
932 "author": f.user(),
907 "desc": f.description(),
933 "desc": f.description(),
908 "extra": f.extra(),
934 "extra": f.extra(),
909 "file": f.path(),
935 "file": f.path(),
910 "targetline": targetline,
936 "targetline": targetline,
911 "line": l,
937 "line": l,
912 "lineno": lineno + 1,
938 "lineno": lineno + 1,
913 "lineid": "l%d" % (lineno + 1),
939 "lineid": "l%d" % (lineno + 1),
914 "linenumber": "% 6d" % (lineno + 1),
940 "linenumber": "% 6d" % (lineno + 1),
915 "revdate": f.date()}
941 "revdate": f.date()}
916
942
917 return tmpl("fileannotate",
943 return tmpl("fileannotate",
918 file=f,
944 file=f,
919 annotate=annotate,
945 annotate=annotate,
920 path=webutil.up(f),
946 path=webutil.up(f),
921 rev=fctx.rev(),
947 rev=fctx.rev(),
922 symrev=webutil.symrevorshortnode(req, fctx),
948 symrev=webutil.symrevorshortnode(req, fctx),
923 node=fctx.hex(),
949 node=fctx.hex(),
924 author=fctx.user(),
950 author=fctx.user(),
925 date=fctx.date(),
951 date=fctx.date(),
926 desc=fctx.description(),
952 desc=fctx.description(),
927 extra=fctx.extra(),
953 extra=fctx.extra(),
928 rename=webutil.renamelink(fctx),
954 rename=webutil.renamelink(fctx),
929 branch=webutil.nodebranchnodefault(fctx),
955 branch=webutil.nodebranchnodefault(fctx),
930 parent=webutil.parents(fctx),
956 parent=webutil.parents(fctx),
931 child=webutil.children(fctx),
957 child=webutil.children(fctx),
932 tags=webutil.nodetagsdict(web.repo, fctx.node()),
958 tags=webutil.nodetagsdict(web.repo, fctx.node()),
933 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
959 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
934 permissions=fctx.manifest().flags(f))
960 permissions=fctx.manifest().flags(f))
935
961
936 @webcommand('filelog')
962 @webcommand('filelog')
937 def filelog(web, req, tmpl):
963 def filelog(web, req, tmpl):
938 """
964 """
939 /filelog/{revision}/{path}
965 /filelog/{revision}/{path}
940 --------------------------
966 --------------------------
941
967
942 Show information about the history of a file in the repository.
968 Show information about the history of a file in the repository.
943
969
944 The ``revcount`` query string argument can be defined to control the
970 The ``revcount`` query string argument can be defined to control the
945 maximum number of entries to show.
971 maximum number of entries to show.
946
972
947 The ``filelog`` template will be rendered.
973 The ``filelog`` template will be rendered.
948 """
974 """
949
975
950 try:
976 try:
951 fctx = webutil.filectx(web.repo, req)
977 fctx = webutil.filectx(web.repo, req)
952 f = fctx.path()
978 f = fctx.path()
953 fl = fctx.filelog()
979 fl = fctx.filelog()
954 except error.LookupError:
980 except error.LookupError:
955 f = webutil.cleanpath(web.repo, req.form['file'][0])
981 f = webutil.cleanpath(web.repo, req.form['file'][0])
956 fl = web.repo.file(f)
982 fl = web.repo.file(f)
957 numrevs = len(fl)
983 numrevs = len(fl)
958 if not numrevs: # file doesn't exist at all
984 if not numrevs: # file doesn't exist at all
959 raise
985 raise
960 rev = webutil.changectx(web.repo, req).rev()
986 rev = webutil.changectx(web.repo, req).rev()
961 first = fl.linkrev(0)
987 first = fl.linkrev(0)
962 if rev < first: # current rev is from before file existed
988 if rev < first: # current rev is from before file existed
963 raise
989 raise
964 frev = numrevs - 1
990 frev = numrevs - 1
965 while fl.linkrev(frev) > rev:
991 while fl.linkrev(frev) > rev:
966 frev -= 1
992 frev -= 1
967 fctx = web.repo.filectx(f, fl.linkrev(frev))
993 fctx = web.repo.filectx(f, fl.linkrev(frev))
968
994
969 revcount = web.maxshortchanges
995 revcount = web.maxshortchanges
970 if 'revcount' in req.form:
996 if 'revcount' in req.form:
971 try:
997 try:
972 revcount = int(req.form.get('revcount', [revcount])[0])
998 revcount = int(req.form.get('revcount', [revcount])[0])
973 revcount = max(revcount, 1)
999 revcount = max(revcount, 1)
974 tmpl.defaults['sessionvars']['revcount'] = revcount
1000 tmpl.defaults['sessionvars']['revcount'] = revcount
975 except ValueError:
1001 except ValueError:
976 pass
1002 pass
977
1003
978 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1004 lessvars = copy.copy(tmpl.defaults['sessionvars'])
979 lessvars['revcount'] = max(revcount / 2, 1)
1005 lessvars['revcount'] = max(revcount / 2, 1)
980 morevars = copy.copy(tmpl.defaults['sessionvars'])
1006 morevars = copy.copy(tmpl.defaults['sessionvars'])
981 morevars['revcount'] = revcount * 2
1007 morevars['revcount'] = revcount * 2
982
1008
983 count = fctx.filerev() + 1
1009 count = fctx.filerev() + 1
984 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
1010 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
985 end = min(count, start + revcount) # last rev on this page
1011 end = min(count, start + revcount) # last rev on this page
986 parity = paritygen(web.stripecount, offset=start - end)
1012 parity = paritygen(web.stripecount, offset=start - end)
987
1013
988 def entries():
1014 def entries():
989 l = []
1015 l = []
990
1016
991 repo = web.repo
1017 repo = web.repo
992 revs = fctx.filelog().revs(start, end - 1)
1018 revs = fctx.filelog().revs(start, end - 1)
993 for i in revs:
1019 for i in revs:
994 iterfctx = fctx.filectx(i)
1020 iterfctx = fctx.filectx(i)
995
1021
996 l.append({"parity": parity.next(),
1022 l.append({"parity": parity.next(),
997 "filerev": i,
1023 "filerev": i,
998 "file": f,
1024 "file": f,
999 "node": iterfctx.hex(),
1025 "node": iterfctx.hex(),
1000 "author": iterfctx.user(),
1026 "author": iterfctx.user(),
1001 "date": iterfctx.date(),
1027 "date": iterfctx.date(),
1002 "rename": webutil.renamelink(iterfctx),
1028 "rename": webutil.renamelink(iterfctx),
1003 "parent": lambda **x: webutil.parents(iterfctx),
1029 "parent": lambda **x: webutil.parents(iterfctx),
1004 "child": lambda **x: webutil.children(iterfctx),
1030 "child": lambda **x: webutil.children(iterfctx),
1005 "desc": iterfctx.description(),
1031 "desc": iterfctx.description(),
1006 "extra": iterfctx.extra(),
1032 "extra": iterfctx.extra(),
1007 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1033 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1008 "bookmarks": webutil.nodebookmarksdict(
1034 "bookmarks": webutil.nodebookmarksdict(
1009 repo, iterfctx.node()),
1035 repo, iterfctx.node()),
1010 "branch": webutil.nodebranchnodefault(iterfctx),
1036 "branch": webutil.nodebranchnodefault(iterfctx),
1011 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1037 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1012 "branches": webutil.nodebranchdict(repo, iterfctx)})
1038 "branches": webutil.nodebranchdict(repo, iterfctx)})
1013 for e in reversed(l):
1039 for e in reversed(l):
1014 yield e
1040 yield e
1015
1041
1016 entries = list(entries())
1042 entries = list(entries())
1017 latestentry = entries[:1]
1043 latestentry = entries[:1]
1018
1044
1019 revnav = webutil.filerevnav(web.repo, fctx.path())
1045 revnav = webutil.filerevnav(web.repo, fctx.path())
1020 nav = revnav.gen(end - 1, revcount, count)
1046 nav = revnav.gen(end - 1, revcount, count)
1021 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1047 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1022 symrev=webutil.symrevorshortnode(req, fctx),
1048 symrev=webutil.symrevorshortnode(req, fctx),
1023 entries=entries,
1049 entries=entries,
1024 latestentry=latestentry,
1050 latestentry=latestentry,
1025 revcount=revcount, morevars=morevars, lessvars=lessvars)
1051 revcount=revcount, morevars=morevars, lessvars=lessvars)
1026
1052
1027 @webcommand('archive')
1053 @webcommand('archive')
1028 def archive(web, req, tmpl):
1054 def archive(web, req, tmpl):
1029 """
1055 """
1030 /archive/{revision}.{format}[/{path}]
1056 /archive/{revision}.{format}[/{path}]
1031 -------------------------------------
1057 -------------------------------------
1032
1058
1033 Obtain an archive of repository content.
1059 Obtain an archive of repository content.
1034
1060
1035 The content and type of the archive is defined by a URL path parameter.
1061 The content and type of the archive is defined by a URL path parameter.
1036 ``format`` is the file extension of the archive type to be generated. e.g.
1062 ``format`` is the file extension of the archive type to be generated. e.g.
1037 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1063 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1038 server configuration.
1064 server configuration.
1039
1065
1040 The optional ``path`` URL parameter controls content to include in the
1066 The optional ``path`` URL parameter controls content to include in the
1041 archive. If omitted, every file in the specified revision is present in the
1067 archive. If omitted, every file in the specified revision is present in the
1042 archive. If included, only the specified file or contents of the specified
1068 archive. If included, only the specified file or contents of the specified
1043 directory will be included in the archive.
1069 directory will be included in the archive.
1044
1070
1045 No template is used for this handler. Raw, binary content is generated.
1071 No template is used for this handler. Raw, binary content is generated.
1046 """
1072 """
1047
1073
1048 type_ = req.form.get('type', [None])[0]
1074 type_ = req.form.get('type', [None])[0]
1049 allowed = web.configlist("web", "allow_archive")
1075 allowed = web.configlist("web", "allow_archive")
1050 key = req.form['node'][0]
1076 key = req.form['node'][0]
1051
1077
1052 if type_ not in web.archives:
1078 if type_ not in web.archives:
1053 msg = 'Unsupported archive type: %s' % type_
1079 msg = 'Unsupported archive type: %s' % type_
1054 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1080 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1055
1081
1056 if not ((type_ in allowed or
1082 if not ((type_ in allowed or
1057 web.configbool("web", "allow" + type_, False))):
1083 web.configbool("web", "allow" + type_, False))):
1058 msg = 'Archive type not allowed: %s' % type_
1084 msg = 'Archive type not allowed: %s' % type_
1059 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1085 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1060
1086
1061 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1087 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1062 cnode = web.repo.lookup(key)
1088 cnode = web.repo.lookup(key)
1063 arch_version = key
1089 arch_version = key
1064 if cnode == key or key == 'tip':
1090 if cnode == key or key == 'tip':
1065 arch_version = short(cnode)
1091 arch_version = short(cnode)
1066 name = "%s-%s" % (reponame, arch_version)
1092 name = "%s-%s" % (reponame, arch_version)
1067
1093
1068 ctx = webutil.changectx(web.repo, req)
1094 ctx = webutil.changectx(web.repo, req)
1069 pats = []
1095 pats = []
1070 matchfn = scmutil.match(ctx, [])
1096 matchfn = scmutil.match(ctx, [])
1071 file = req.form.get('file', None)
1097 file = req.form.get('file', None)
1072 if file:
1098 if file:
1073 pats = ['path:' + file[0]]
1099 pats = ['path:' + file[0]]
1074 matchfn = scmutil.match(ctx, pats, default='path')
1100 matchfn = scmutil.match(ctx, pats, default='path')
1075 if pats:
1101 if pats:
1076 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1102 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1077 if not files:
1103 if not files:
1078 raise ErrorResponse(HTTP_NOT_FOUND,
1104 raise ErrorResponse(HTTP_NOT_FOUND,
1079 'file(s) not found: %s' % file[0])
1105 'file(s) not found: %s' % file[0])
1080
1106
1081 mimetype, artype, extension, encoding = web.archivespecs[type_]
1107 mimetype, artype, extension, encoding = web.archivespecs[type_]
1082 headers = [
1108 headers = [
1083 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1109 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1084 ]
1110 ]
1085 if encoding:
1111 if encoding:
1086 headers.append(('Content-Encoding', encoding))
1112 headers.append(('Content-Encoding', encoding))
1087 req.headers.extend(headers)
1113 req.headers.extend(headers)
1088 req.respond(HTTP_OK, mimetype)
1114 req.respond(HTTP_OK, mimetype)
1089
1115
1090 archival.archive(web.repo, req, cnode, artype, prefix=name,
1116 archival.archive(web.repo, req, cnode, artype, prefix=name,
1091 matchfn=matchfn,
1117 matchfn=matchfn,
1092 subrepos=web.configbool("web", "archivesubrepos"))
1118 subrepos=web.configbool("web", "archivesubrepos"))
1093 return []
1119 return []
1094
1120
1095
1121
1096 @webcommand('static')
1122 @webcommand('static')
1097 def static(web, req, tmpl):
1123 def static(web, req, tmpl):
1098 fname = req.form['file'][0]
1124 fname = req.form['file'][0]
1099 # a repo owner may set web.static in .hg/hgrc to get any file
1125 # a repo owner may set web.static in .hg/hgrc to get any file
1100 # readable by the user running the CGI script
1126 # readable by the user running the CGI script
1101 static = web.config("web", "static", None, untrusted=False)
1127 static = web.config("web", "static", None, untrusted=False)
1102 if not static:
1128 if not static:
1103 tp = web.templatepath or templater.templatepaths()
1129 tp = web.templatepath or templater.templatepaths()
1104 if isinstance(tp, str):
1130 if isinstance(tp, str):
1105 tp = [tp]
1131 tp = [tp]
1106 static = [os.path.join(p, 'static') for p in tp]
1132 static = [os.path.join(p, 'static') for p in tp]
1107 staticfile(static, fname, req)
1133 staticfile(static, fname, req)
1108 return []
1134 return []
1109
1135
1110 @webcommand('graph')
1136 @webcommand('graph')
1111 def graph(web, req, tmpl):
1137 def graph(web, req, tmpl):
1112 """
1138 """
1113 /graph[/{revision}]
1139 /graph[/{revision}]
1114 -------------------
1140 -------------------
1115
1141
1116 Show information about the graphical topology of the repository.
1142 Show information about the graphical topology of the repository.
1117
1143
1118 Information rendered by this handler can be used to create visual
1144 Information rendered by this handler can be used to create visual
1119 representations of repository topology.
1145 representations of repository topology.
1120
1146
1121 The ``revision`` URL parameter controls the starting changeset.
1147 The ``revision`` URL parameter controls the starting changeset.
1122
1148
1123 The ``revcount`` query string argument can define the number of changesets
1149 The ``revcount`` query string argument can define the number of changesets
1124 to show information for.
1150 to show information for.
1125
1151
1126 This handler will render the ``graph`` template.
1152 This handler will render the ``graph`` template.
1127 """
1153 """
1128
1154
1129 if 'node' in req.form:
1155 if 'node' in req.form:
1130 ctx = webutil.changectx(web.repo, req)
1156 ctx = webutil.changectx(web.repo, req)
1131 symrev = webutil.symrevorshortnode(req, ctx)
1157 symrev = webutil.symrevorshortnode(req, ctx)
1132 else:
1158 else:
1133 ctx = web.repo['tip']
1159 ctx = web.repo['tip']
1134 symrev = 'tip'
1160 symrev = 'tip'
1135 rev = ctx.rev()
1161 rev = ctx.rev()
1136
1162
1137 bg_height = 39
1163 bg_height = 39
1138 revcount = web.maxshortchanges
1164 revcount = web.maxshortchanges
1139 if 'revcount' in req.form:
1165 if 'revcount' in req.form:
1140 try:
1166 try:
1141 revcount = int(req.form.get('revcount', [revcount])[0])
1167 revcount = int(req.form.get('revcount', [revcount])[0])
1142 revcount = max(revcount, 1)
1168 revcount = max(revcount, 1)
1143 tmpl.defaults['sessionvars']['revcount'] = revcount
1169 tmpl.defaults['sessionvars']['revcount'] = revcount
1144 except ValueError:
1170 except ValueError:
1145 pass
1171 pass
1146
1172
1147 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1173 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1148 lessvars['revcount'] = max(revcount / 2, 1)
1174 lessvars['revcount'] = max(revcount / 2, 1)
1149 morevars = copy.copy(tmpl.defaults['sessionvars'])
1175 morevars = copy.copy(tmpl.defaults['sessionvars'])
1150 morevars['revcount'] = revcount * 2
1176 morevars['revcount'] = revcount * 2
1151
1177
1152 count = len(web.repo)
1178 count = len(web.repo)
1153 pos = rev
1179 pos = rev
1154
1180
1155 uprev = min(max(0, count - 1), rev + revcount)
1181 uprev = min(max(0, count - 1), rev + revcount)
1156 downrev = max(0, rev - revcount)
1182 downrev = max(0, rev - revcount)
1157 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1183 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1158
1184
1159 tree = []
1185 tree = []
1160 if pos != -1:
1186 if pos != -1:
1161 allrevs = web.repo.changelog.revs(pos, 0)
1187 allrevs = web.repo.changelog.revs(pos, 0)
1162 revs = []
1188 revs = []
1163 for i in allrevs:
1189 for i in allrevs:
1164 revs.append(i)
1190 revs.append(i)
1165 if len(revs) >= revcount:
1191 if len(revs) >= revcount:
1166 break
1192 break
1167
1193
1168 # We have to feed a baseset to dagwalker as it is expecting smartset
1194 # We have to feed a baseset to dagwalker as it is expecting smartset
1169 # object. This does not have a big impact on hgweb performance itself
1195 # object. This does not have a big impact on hgweb performance itself
1170 # since hgweb graphing code is not itself lazy yet.
1196 # since hgweb graphing code is not itself lazy yet.
1171 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1197 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1172 # As we said one line above... not lazy.
1198 # As we said one line above... not lazy.
1173 tree = list(graphmod.colored(dag, web.repo))
1199 tree = list(graphmod.colored(dag, web.repo))
1174
1200
1175 def getcolumns(tree):
1201 def getcolumns(tree):
1176 cols = 0
1202 cols = 0
1177 for (id, type, ctx, vtx, edges) in tree:
1203 for (id, type, ctx, vtx, edges) in tree:
1178 if type != graphmod.CHANGESET:
1204 if type != graphmod.CHANGESET:
1179 continue
1205 continue
1180 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1206 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1181 max([edge[1] for edge in edges] or [0]))
1207 max([edge[1] for edge in edges] or [0]))
1182 return cols
1208 return cols
1183
1209
1184 def graphdata(usetuples, **map):
1210 def graphdata(usetuples, **map):
1185 data = []
1211 data = []
1186
1212
1187 row = 0
1213 row = 0
1188 for (id, type, ctx, vtx, edges) in tree:
1214 for (id, type, ctx, vtx, edges) in tree:
1189 if type != graphmod.CHANGESET:
1215 if type != graphmod.CHANGESET:
1190 continue
1216 continue
1191 node = str(ctx)
1217 node = str(ctx)
1192 age = templatefilters.age(ctx.date())
1218 age = templatefilters.age(ctx.date())
1193 desc = templatefilters.firstline(ctx.description())
1219 desc = templatefilters.firstline(ctx.description())
1194 desc = cgi.escape(templatefilters.nonempty(desc))
1220 desc = cgi.escape(templatefilters.nonempty(desc))
1195 user = cgi.escape(templatefilters.person(ctx.user()))
1221 user = cgi.escape(templatefilters.person(ctx.user()))
1196 branch = cgi.escape(ctx.branch())
1222 branch = cgi.escape(ctx.branch())
1197 try:
1223 try:
1198 branchnode = web.repo.branchtip(branch)
1224 branchnode = web.repo.branchtip(branch)
1199 except error.RepoLookupError:
1225 except error.RepoLookupError:
1200 branchnode = None
1226 branchnode = None
1201 branch = branch, branchnode == ctx.node()
1227 branch = branch, branchnode == ctx.node()
1202
1228
1203 if usetuples:
1229 if usetuples:
1204 data.append((node, vtx, edges, desc, user, age, branch,
1230 data.append((node, vtx, edges, desc, user, age, branch,
1205 [cgi.escape(x) for x in ctx.tags()],
1231 [cgi.escape(x) for x in ctx.tags()],
1206 [cgi.escape(x) for x in ctx.bookmarks()]))
1232 [cgi.escape(x) for x in ctx.bookmarks()]))
1207 else:
1233 else:
1208 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1234 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1209 'color': (edge[2] - 1) % 6 + 1,
1235 'color': (edge[2] - 1) % 6 + 1,
1210 'width': edge[3], 'bcolor': edge[4]}
1236 'width': edge[3], 'bcolor': edge[4]}
1211 for edge in edges]
1237 for edge in edges]
1212
1238
1213 data.append(
1239 data.append(
1214 {'node': node,
1240 {'node': node,
1215 'col': vtx[0],
1241 'col': vtx[0],
1216 'color': (vtx[1] - 1) % 6 + 1,
1242 'color': (vtx[1] - 1) % 6 + 1,
1217 'edges': edgedata,
1243 'edges': edgedata,
1218 'row': row,
1244 'row': row,
1219 'nextrow': row + 1,
1245 'nextrow': row + 1,
1220 'desc': desc,
1246 'desc': desc,
1221 'user': user,
1247 'user': user,
1222 'age': age,
1248 'age': age,
1223 'bookmarks': webutil.nodebookmarksdict(
1249 'bookmarks': webutil.nodebookmarksdict(
1224 web.repo, ctx.node()),
1250 web.repo, ctx.node()),
1225 'branches': webutil.nodebranchdict(web.repo, ctx),
1251 'branches': webutil.nodebranchdict(web.repo, ctx),
1226 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1252 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1227 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1253 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1228
1254
1229 row += 1
1255 row += 1
1230
1256
1231 return data
1257 return data
1232
1258
1233 cols = getcolumns(tree)
1259 cols = getcolumns(tree)
1234 rows = len(tree)
1260 rows = len(tree)
1235 canvasheight = (rows + 1) * bg_height - 27
1261 canvasheight = (rows + 1) * bg_height - 27
1236
1262
1237 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1263 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1238 uprev=uprev,
1264 uprev=uprev,
1239 lessvars=lessvars, morevars=morevars, downrev=downrev,
1265 lessvars=lessvars, morevars=morevars, downrev=downrev,
1240 cols=cols, rows=rows,
1266 cols=cols, rows=rows,
1241 canvaswidth=(cols + 1) * bg_height,
1267 canvaswidth=(cols + 1) * bg_height,
1242 truecanvasheight=rows * bg_height,
1268 truecanvasheight=rows * bg_height,
1243 canvasheight=canvasheight, bg_height=bg_height,
1269 canvasheight=canvasheight, bg_height=bg_height,
1244 jsdata=lambda **x: graphdata(True, **x),
1270 jsdata=lambda **x: graphdata(True, **x),
1245 nodes=lambda **x: graphdata(False, **x),
1271 nodes=lambda **x: graphdata(False, **x),
1246 node=ctx.hex(), changenav=changenav)
1272 node=ctx.hex(), changenav=changenav)
1247
1273
1248 def _getdoc(e):
1274 def _getdoc(e):
1249 doc = e[0].__doc__
1275 doc = e[0].__doc__
1250 if doc:
1276 if doc:
1251 doc = _(doc).partition('\n')[0]
1277 doc = _(doc).partition('\n')[0]
1252 else:
1278 else:
1253 doc = _('(no help text available)')
1279 doc = _('(no help text available)')
1254 return doc
1280 return doc
1255
1281
1256 @webcommand('help')
1282 @webcommand('help')
1257 def help(web, req, tmpl):
1283 def help(web, req, tmpl):
1258 """
1284 """
1259 /help[/{topic}]
1285 /help[/{topic}]
1260 ---------------
1286 ---------------
1261
1287
1262 Render help documentation.
1288 Render help documentation.
1263
1289
1264 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1290 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1265 is defined, that help topic will be rendered. If not, an index of
1291 is defined, that help topic will be rendered. If not, an index of
1266 available help topics will be rendered.
1292 available help topics will be rendered.
1267
1293
1268 The ``help`` template will be rendered when requesting help for a topic.
1294 The ``help`` template will be rendered when requesting help for a topic.
1269 ``helptopics`` will be rendered for the index of help topics.
1295 ``helptopics`` will be rendered for the index of help topics.
1270 """
1296 """
1271 from mercurial import commands # avoid cycle
1297 from .. import commands, help as helpmod # avoid cycle
1272 from mercurial import help as helpmod # avoid cycle
1273
1298
1274 topicname = req.form.get('node', [None])[0]
1299 topicname = req.form.get('node', [None])[0]
1275 if not topicname:
1300 if not topicname:
1276 def topics(**map):
1301 def topics(**map):
1277 for entries, summary, _doc in helpmod.helptable:
1302 for entries, summary, _doc in helpmod.helptable:
1278 yield {'topic': entries[0], 'summary': summary}
1303 yield {'topic': entries[0], 'summary': summary}
1279
1304
1280 early, other = [], []
1305 early, other = [], []
1281 primary = lambda s: s.partition('|')[0]
1306 primary = lambda s: s.partition('|')[0]
1282 for c, e in commands.table.iteritems():
1307 for c, e in commands.table.iteritems():
1283 doc = _getdoc(e)
1308 doc = _getdoc(e)
1284 if 'DEPRECATED' in doc or c.startswith('debug'):
1309 if 'DEPRECATED' in doc or c.startswith('debug'):
1285 continue
1310 continue
1286 cmd = primary(c)
1311 cmd = primary(c)
1287 if cmd.startswith('^'):
1312 if cmd.startswith('^'):
1288 early.append((cmd[1:], doc))
1313 early.append((cmd[1:], doc))
1289 else:
1314 else:
1290 other.append((cmd, doc))
1315 other.append((cmd, doc))
1291
1316
1292 early.sort()
1317 early.sort()
1293 other.sort()
1318 other.sort()
1294
1319
1295 def earlycommands(**map):
1320 def earlycommands(**map):
1296 for c, doc in early:
1321 for c, doc in early:
1297 yield {'topic': c, 'summary': doc}
1322 yield {'topic': c, 'summary': doc}
1298
1323
1299 def othercommands(**map):
1324 def othercommands(**map):
1300 for c, doc in other:
1325 for c, doc in other:
1301 yield {'topic': c, 'summary': doc}
1326 yield {'topic': c, 'summary': doc}
1302
1327
1303 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1328 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1304 othercommands=othercommands, title='Index')
1329 othercommands=othercommands, title='Index')
1305
1330
1306 u = webutil.wsgiui()
1331 u = webutil.wsgiui()
1307 u.verbose = True
1332 u.verbose = True
1308 try:
1333 try:
1309 doc = helpmod.help_(u, topicname)
1334 doc = helpmod.help_(u, topicname)
1310 except error.UnknownCommand:
1335 except error.UnknownCommand:
1311 raise ErrorResponse(HTTP_NOT_FOUND)
1336 raise ErrorResponse(HTTP_NOT_FOUND)
1312 return tmpl('help', topic=topicname, doc=doc)
1337 return tmpl('help', topic=topicname, doc=doc)
1313
1338
1314 # tell hggettext to extract docstrings from these functions:
1339 # tell hggettext to extract docstrings from these functions:
1315 i18nfunctions = commands.values()
1340 i18nfunctions = commands.values()
@@ -1,593 +1,609 b''
1 # hgweb/webutil.py - utility library for the web interface.
1 # hgweb/webutil.py - utility library for the web interface.
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-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 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, copy
9 from __future__ import absolute_import
10
11 import copy
12 import difflib
13 import os
10 import re
14 import re
11 from mercurial import match, patch, error, util, pathutil, context
15
12 from mercurial import ui as uimod
16 from ..i18n import _
13 from mercurial.i18n import _
17 from ..node import hex, nullid, short
14 from mercurial.node import hex, nullid, short
18
15 from mercurial import templatefilters
19 from .common import (
16 from common import ErrorResponse, paritygen
20 ErrorResponse,
17 from common import HTTP_NOT_FOUND
21 HTTP_NOT_FOUND,
18 import difflib
22 paritygen,
23 )
24
25 from .. import (
26 context,
27 error,
28 match,
29 patch,
30 pathutil,
31 templatefilters,
32 ui as uimod,
33 util,
34 )
19
35
20 def up(p):
36 def up(p):
21 if p[0] != "/":
37 if p[0] != "/":
22 p = "/" + p
38 p = "/" + p
23 if p[-1] == "/":
39 if p[-1] == "/":
24 p = p[:-1]
40 p = p[:-1]
25 up = os.path.dirname(p)
41 up = os.path.dirname(p)
26 if up == "/":
42 if up == "/":
27 return "/"
43 return "/"
28 return up + "/"
44 return up + "/"
29
45
30 def _navseq(step, firststep=None):
46 def _navseq(step, firststep=None):
31 if firststep:
47 if firststep:
32 yield firststep
48 yield firststep
33 if firststep >= 20 and firststep <= 40:
49 if firststep >= 20 and firststep <= 40:
34 firststep = 50
50 firststep = 50
35 yield firststep
51 yield firststep
36 assert step > 0
52 assert step > 0
37 assert firststep > 0
53 assert firststep > 0
38 while step <= firststep:
54 while step <= firststep:
39 step *= 10
55 step *= 10
40 while True:
56 while True:
41 yield 1 * step
57 yield 1 * step
42 yield 3 * step
58 yield 3 * step
43 step *= 10
59 step *= 10
44
60
45 class revnav(object):
61 class revnav(object):
46
62
47 def __init__(self, repo):
63 def __init__(self, repo):
48 """Navigation generation object
64 """Navigation generation object
49
65
50 :repo: repo object we generate nav for
66 :repo: repo object we generate nav for
51 """
67 """
52 # used for hex generation
68 # used for hex generation
53 self._revlog = repo.changelog
69 self._revlog = repo.changelog
54
70
55 def __nonzero__(self):
71 def __nonzero__(self):
56 """return True if any revision to navigate over"""
72 """return True if any revision to navigate over"""
57 return self._first() is not None
73 return self._first() is not None
58
74
59 def _first(self):
75 def _first(self):
60 """return the minimum non-filtered changeset or None"""
76 """return the minimum non-filtered changeset or None"""
61 try:
77 try:
62 return iter(self._revlog).next()
78 return iter(self._revlog).next()
63 except StopIteration:
79 except StopIteration:
64 return None
80 return None
65
81
66 def hex(self, rev):
82 def hex(self, rev):
67 return hex(self._revlog.node(rev))
83 return hex(self._revlog.node(rev))
68
84
69 def gen(self, pos, pagelen, limit):
85 def gen(self, pos, pagelen, limit):
70 """computes label and revision id for navigation link
86 """computes label and revision id for navigation link
71
87
72 :pos: is the revision relative to which we generate navigation.
88 :pos: is the revision relative to which we generate navigation.
73 :pagelen: the size of each navigation page
89 :pagelen: the size of each navigation page
74 :limit: how far shall we link
90 :limit: how far shall we link
75
91
76 The return is:
92 The return is:
77 - a single element tuple
93 - a single element tuple
78 - containing a dictionary with a `before` and `after` key
94 - containing a dictionary with a `before` and `after` key
79 - values are generator functions taking arbitrary number of kwargs
95 - values are generator functions taking arbitrary number of kwargs
80 - yield items are dictionaries with `label` and `node` keys
96 - yield items are dictionaries with `label` and `node` keys
81 """
97 """
82 if not self:
98 if not self:
83 # empty repo
99 # empty repo
84 return ({'before': (), 'after': ()},)
100 return ({'before': (), 'after': ()},)
85
101
86 targets = []
102 targets = []
87 for f in _navseq(1, pagelen):
103 for f in _navseq(1, pagelen):
88 if f > limit:
104 if f > limit:
89 break
105 break
90 targets.append(pos + f)
106 targets.append(pos + f)
91 targets.append(pos - f)
107 targets.append(pos - f)
92 targets.sort()
108 targets.sort()
93
109
94 first = self._first()
110 first = self._first()
95 navbefore = [("(%i)" % first, self.hex(first))]
111 navbefore = [("(%i)" % first, self.hex(first))]
96 navafter = []
112 navafter = []
97 for rev in targets:
113 for rev in targets:
98 if rev not in self._revlog:
114 if rev not in self._revlog:
99 continue
115 continue
100 if pos < rev < limit:
116 if pos < rev < limit:
101 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
117 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
102 if 0 < rev < pos:
118 if 0 < rev < pos:
103 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
119 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
104
120
105
121
106 navafter.append(("tip", "tip"))
122 navafter.append(("tip", "tip"))
107
123
108 data = lambda i: {"label": i[0], "node": i[1]}
124 data = lambda i: {"label": i[0], "node": i[1]}
109 return ({'before': lambda **map: (data(i) for i in navbefore),
125 return ({'before': lambda **map: (data(i) for i in navbefore),
110 'after': lambda **map: (data(i) for i in navafter)},)
126 'after': lambda **map: (data(i) for i in navafter)},)
111
127
112 class filerevnav(revnav):
128 class filerevnav(revnav):
113
129
114 def __init__(self, repo, path):
130 def __init__(self, repo, path):
115 """Navigation generation object
131 """Navigation generation object
116
132
117 :repo: repo object we generate nav for
133 :repo: repo object we generate nav for
118 :path: path of the file we generate nav for
134 :path: path of the file we generate nav for
119 """
135 """
120 # used for iteration
136 # used for iteration
121 self._changelog = repo.unfiltered().changelog
137 self._changelog = repo.unfiltered().changelog
122 # used for hex generation
138 # used for hex generation
123 self._revlog = repo.file(path)
139 self._revlog = repo.file(path)
124
140
125 def hex(self, rev):
141 def hex(self, rev):
126 return hex(self._changelog.node(self._revlog.linkrev(rev)))
142 return hex(self._changelog.node(self._revlog.linkrev(rev)))
127
143
128 class _siblings(object):
144 class _siblings(object):
129 def __init__(self, siblings=[], hiderev=None):
145 def __init__(self, siblings=[], hiderev=None):
130 self.siblings = [s for s in siblings if s.node() != nullid]
146 self.siblings = [s for s in siblings if s.node() != nullid]
131 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
147 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
132 self.siblings = []
148 self.siblings = []
133
149
134 def __iter__(self):
150 def __iter__(self):
135 for s in self.siblings:
151 for s in self.siblings:
136 d = {
152 d = {
137 'node': s.hex(),
153 'node': s.hex(),
138 'rev': s.rev(),
154 'rev': s.rev(),
139 'user': s.user(),
155 'user': s.user(),
140 'date': s.date(),
156 'date': s.date(),
141 'description': s.description(),
157 'description': s.description(),
142 'branch': s.branch(),
158 'branch': s.branch(),
143 }
159 }
144 if util.safehasattr(s, 'path'):
160 if util.safehasattr(s, 'path'):
145 d['file'] = s.path()
161 d['file'] = s.path()
146 yield d
162 yield d
147
163
148 def __len__(self):
164 def __len__(self):
149 return len(self.siblings)
165 return len(self.siblings)
150
166
151 def parents(ctx, hide=None):
167 def parents(ctx, hide=None):
152 if isinstance(ctx, context.basefilectx):
168 if isinstance(ctx, context.basefilectx):
153 introrev = ctx.introrev()
169 introrev = ctx.introrev()
154 if ctx.changectx().rev() != introrev:
170 if ctx.changectx().rev() != introrev:
155 return _siblings([ctx.repo()[introrev]], hide)
171 return _siblings([ctx.repo()[introrev]], hide)
156 return _siblings(ctx.parents(), hide)
172 return _siblings(ctx.parents(), hide)
157
173
158 def children(ctx, hide=None):
174 def children(ctx, hide=None):
159 return _siblings(ctx.children(), hide)
175 return _siblings(ctx.children(), hide)
160
176
161 def renamelink(fctx):
177 def renamelink(fctx):
162 r = fctx.renamed()
178 r = fctx.renamed()
163 if r:
179 if r:
164 return [{'file': r[0], 'node': hex(r[1])}]
180 return [{'file': r[0], 'node': hex(r[1])}]
165 return []
181 return []
166
182
167 def nodetagsdict(repo, node):
183 def nodetagsdict(repo, node):
168 return [{"name": i} for i in repo.nodetags(node)]
184 return [{"name": i} for i in repo.nodetags(node)]
169
185
170 def nodebookmarksdict(repo, node):
186 def nodebookmarksdict(repo, node):
171 return [{"name": i} for i in repo.nodebookmarks(node)]
187 return [{"name": i} for i in repo.nodebookmarks(node)]
172
188
173 def nodebranchdict(repo, ctx):
189 def nodebranchdict(repo, ctx):
174 branches = []
190 branches = []
175 branch = ctx.branch()
191 branch = ctx.branch()
176 # If this is an empty repo, ctx.node() == nullid,
192 # If this is an empty repo, ctx.node() == nullid,
177 # ctx.branch() == 'default'.
193 # ctx.branch() == 'default'.
178 try:
194 try:
179 branchnode = repo.branchtip(branch)
195 branchnode = repo.branchtip(branch)
180 except error.RepoLookupError:
196 except error.RepoLookupError:
181 branchnode = None
197 branchnode = None
182 if branchnode == ctx.node():
198 if branchnode == ctx.node():
183 branches.append({"name": branch})
199 branches.append({"name": branch})
184 return branches
200 return branches
185
201
186 def nodeinbranch(repo, ctx):
202 def nodeinbranch(repo, ctx):
187 branches = []
203 branches = []
188 branch = ctx.branch()
204 branch = ctx.branch()
189 try:
205 try:
190 branchnode = repo.branchtip(branch)
206 branchnode = repo.branchtip(branch)
191 except error.RepoLookupError:
207 except error.RepoLookupError:
192 branchnode = None
208 branchnode = None
193 if branch != 'default' and branchnode != ctx.node():
209 if branch != 'default' and branchnode != ctx.node():
194 branches.append({"name": branch})
210 branches.append({"name": branch})
195 return branches
211 return branches
196
212
197 def nodebranchnodefault(ctx):
213 def nodebranchnodefault(ctx):
198 branches = []
214 branches = []
199 branch = ctx.branch()
215 branch = ctx.branch()
200 if branch != 'default':
216 if branch != 'default':
201 branches.append({"name": branch})
217 branches.append({"name": branch})
202 return branches
218 return branches
203
219
204 def showtag(repo, tmpl, t1, node=nullid, **args):
220 def showtag(repo, tmpl, t1, node=nullid, **args):
205 for t in repo.nodetags(node):
221 for t in repo.nodetags(node):
206 yield tmpl(t1, tag=t, **args)
222 yield tmpl(t1, tag=t, **args)
207
223
208 def showbookmark(repo, tmpl, t1, node=nullid, **args):
224 def showbookmark(repo, tmpl, t1, node=nullid, **args):
209 for t in repo.nodebookmarks(node):
225 for t in repo.nodebookmarks(node):
210 yield tmpl(t1, bookmark=t, **args)
226 yield tmpl(t1, bookmark=t, **args)
211
227
212 def branchentries(repo, stripecount, limit=0):
228 def branchentries(repo, stripecount, limit=0):
213 tips = []
229 tips = []
214 heads = repo.heads()
230 heads = repo.heads()
215 parity = paritygen(stripecount)
231 parity = paritygen(stripecount)
216 sortkey = lambda item: (not item[1], item[0].rev())
232 sortkey = lambda item: (not item[1], item[0].rev())
217
233
218 def entries(**map):
234 def entries(**map):
219 count = 0
235 count = 0
220 if not tips:
236 if not tips:
221 for tag, hs, tip, closed in repo.branchmap().iterbranches():
237 for tag, hs, tip, closed in repo.branchmap().iterbranches():
222 tips.append((repo[tip], closed))
238 tips.append((repo[tip], closed))
223 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
239 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
224 if limit > 0 and count >= limit:
240 if limit > 0 and count >= limit:
225 return
241 return
226 count += 1
242 count += 1
227 if closed:
243 if closed:
228 status = 'closed'
244 status = 'closed'
229 elif ctx.node() not in heads:
245 elif ctx.node() not in heads:
230 status = 'inactive'
246 status = 'inactive'
231 else:
247 else:
232 status = 'open'
248 status = 'open'
233 yield {
249 yield {
234 'parity': parity.next(),
250 'parity': parity.next(),
235 'branch': ctx.branch(),
251 'branch': ctx.branch(),
236 'status': status,
252 'status': status,
237 'node': ctx.hex(),
253 'node': ctx.hex(),
238 'date': ctx.date()
254 'date': ctx.date()
239 }
255 }
240
256
241 return entries
257 return entries
242
258
243 def cleanpath(repo, path):
259 def cleanpath(repo, path):
244 path = path.lstrip('/')
260 path = path.lstrip('/')
245 return pathutil.canonpath(repo.root, '', path)
261 return pathutil.canonpath(repo.root, '', path)
246
262
247 def changeidctx(repo, changeid):
263 def changeidctx(repo, changeid):
248 try:
264 try:
249 ctx = repo[changeid]
265 ctx = repo[changeid]
250 except error.RepoError:
266 except error.RepoError:
251 man = repo.manifest
267 man = repo.manifest
252 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
268 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
253
269
254 return ctx
270 return ctx
255
271
256 def changectx(repo, req):
272 def changectx(repo, req):
257 changeid = "tip"
273 changeid = "tip"
258 if 'node' in req.form:
274 if 'node' in req.form:
259 changeid = req.form['node'][0]
275 changeid = req.form['node'][0]
260 ipos = changeid.find(':')
276 ipos = changeid.find(':')
261 if ipos != -1:
277 if ipos != -1:
262 changeid = changeid[(ipos + 1):]
278 changeid = changeid[(ipos + 1):]
263 elif 'manifest' in req.form:
279 elif 'manifest' in req.form:
264 changeid = req.form['manifest'][0]
280 changeid = req.form['manifest'][0]
265
281
266 return changeidctx(repo, changeid)
282 return changeidctx(repo, changeid)
267
283
268 def basechangectx(repo, req):
284 def basechangectx(repo, req):
269 if 'node' in req.form:
285 if 'node' in req.form:
270 changeid = req.form['node'][0]
286 changeid = req.form['node'][0]
271 ipos = changeid.find(':')
287 ipos = changeid.find(':')
272 if ipos != -1:
288 if ipos != -1:
273 changeid = changeid[:ipos]
289 changeid = changeid[:ipos]
274 return changeidctx(repo, changeid)
290 return changeidctx(repo, changeid)
275
291
276 return None
292 return None
277
293
278 def filectx(repo, req):
294 def filectx(repo, req):
279 if 'file' not in req.form:
295 if 'file' not in req.form:
280 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
296 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
281 path = cleanpath(repo, req.form['file'][0])
297 path = cleanpath(repo, req.form['file'][0])
282 if 'node' in req.form:
298 if 'node' in req.form:
283 changeid = req.form['node'][0]
299 changeid = req.form['node'][0]
284 elif 'filenode' in req.form:
300 elif 'filenode' in req.form:
285 changeid = req.form['filenode'][0]
301 changeid = req.form['filenode'][0]
286 else:
302 else:
287 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
303 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
288 try:
304 try:
289 fctx = repo[changeid][path]
305 fctx = repo[changeid][path]
290 except error.RepoError:
306 except error.RepoError:
291 fctx = repo.filectx(path, fileid=changeid)
307 fctx = repo.filectx(path, fileid=changeid)
292
308
293 return fctx
309 return fctx
294
310
295 def changelistentry(web, ctx, tmpl):
311 def changelistentry(web, ctx, tmpl):
296 '''Obtain a dictionary to be used for entries in a changelist.
312 '''Obtain a dictionary to be used for entries in a changelist.
297
313
298 This function is called when producing items for the "entries" list passed
314 This function is called when producing items for the "entries" list passed
299 to the "shortlog" and "changelog" templates.
315 to the "shortlog" and "changelog" templates.
300 '''
316 '''
301 repo = web.repo
317 repo = web.repo
302 rev = ctx.rev()
318 rev = ctx.rev()
303 n = ctx.node()
319 n = ctx.node()
304 showtags = showtag(repo, tmpl, 'changelogtag', n)
320 showtags = showtag(repo, tmpl, 'changelogtag', n)
305 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
321 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
306
322
307 return {
323 return {
308 "author": ctx.user(),
324 "author": ctx.user(),
309 "parent": lambda **x: parents(ctx, rev - 1),
325 "parent": lambda **x: parents(ctx, rev - 1),
310 "child": lambda **x: children(ctx, rev + 1),
326 "child": lambda **x: children(ctx, rev + 1),
311 "changelogtag": showtags,
327 "changelogtag": showtags,
312 "desc": ctx.description(),
328 "desc": ctx.description(),
313 "extra": ctx.extra(),
329 "extra": ctx.extra(),
314 "date": ctx.date(),
330 "date": ctx.date(),
315 "files": files,
331 "files": files,
316 "rev": rev,
332 "rev": rev,
317 "node": hex(n),
333 "node": hex(n),
318 "tags": nodetagsdict(repo, n),
334 "tags": nodetagsdict(repo, n),
319 "bookmarks": nodebookmarksdict(repo, n),
335 "bookmarks": nodebookmarksdict(repo, n),
320 "inbranch": nodeinbranch(repo, ctx),
336 "inbranch": nodeinbranch(repo, ctx),
321 "branches": nodebranchdict(repo, ctx)
337 "branches": nodebranchdict(repo, ctx)
322 }
338 }
323
339
324 def symrevorshortnode(req, ctx):
340 def symrevorshortnode(req, ctx):
325 if 'node' in req.form:
341 if 'node' in req.form:
326 return templatefilters.revescape(req.form['node'][0])
342 return templatefilters.revescape(req.form['node'][0])
327 else:
343 else:
328 return short(ctx.node())
344 return short(ctx.node())
329
345
330 def changesetentry(web, req, tmpl, ctx):
346 def changesetentry(web, req, tmpl, ctx):
331 '''Obtain a dictionary to be used to render the "changeset" template.'''
347 '''Obtain a dictionary to be used to render the "changeset" template.'''
332
348
333 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
349 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
334 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
350 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
335 ctx.node())
351 ctx.node())
336 showbranch = nodebranchnodefault(ctx)
352 showbranch = nodebranchnodefault(ctx)
337
353
338 files = []
354 files = []
339 parity = paritygen(web.stripecount)
355 parity = paritygen(web.stripecount)
340 for blockno, f in enumerate(ctx.files()):
356 for blockno, f in enumerate(ctx.files()):
341 template = f in ctx and 'filenodelink' or 'filenolink'
357 template = f in ctx and 'filenodelink' or 'filenolink'
342 files.append(tmpl(template,
358 files.append(tmpl(template,
343 node=ctx.hex(), file=f, blockno=blockno + 1,
359 node=ctx.hex(), file=f, blockno=blockno + 1,
344 parity=parity.next()))
360 parity=parity.next()))
345
361
346 basectx = basechangectx(web.repo, req)
362 basectx = basechangectx(web.repo, req)
347 if basectx is None:
363 if basectx is None:
348 basectx = ctx.p1()
364 basectx = ctx.p1()
349
365
350 style = web.config('web', 'style', 'paper')
366 style = web.config('web', 'style', 'paper')
351 if 'style' in req.form:
367 if 'style' in req.form:
352 style = req.form['style'][0]
368 style = req.form['style'][0]
353
369
354 parity = paritygen(web.stripecount)
370 parity = paritygen(web.stripecount)
355 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
371 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
356
372
357 parity = paritygen(web.stripecount)
373 parity = paritygen(web.stripecount)
358 diffstatsgen = diffstatgen(ctx, basectx)
374 diffstatsgen = diffstatgen(ctx, basectx)
359 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
375 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
360
376
361 return dict(
377 return dict(
362 diff=diff,
378 diff=diff,
363 rev=ctx.rev(),
379 rev=ctx.rev(),
364 node=ctx.hex(),
380 node=ctx.hex(),
365 symrev=symrevorshortnode(req, ctx),
381 symrev=symrevorshortnode(req, ctx),
366 parent=parents(ctx),
382 parent=parents(ctx),
367 child=children(ctx),
383 child=children(ctx),
368 basenode=basectx.hex(),
384 basenode=basectx.hex(),
369 changesettag=showtags,
385 changesettag=showtags,
370 changesetbookmark=showbookmarks,
386 changesetbookmark=showbookmarks,
371 changesetbranch=showbranch,
387 changesetbranch=showbranch,
372 author=ctx.user(),
388 author=ctx.user(),
373 desc=ctx.description(),
389 desc=ctx.description(),
374 extra=ctx.extra(),
390 extra=ctx.extra(),
375 date=ctx.date(),
391 date=ctx.date(),
376 phase=ctx.phasestr(),
392 phase=ctx.phasestr(),
377 files=files,
393 files=files,
378 diffsummary=lambda **x: diffsummary(diffstatsgen),
394 diffsummary=lambda **x: diffsummary(diffstatsgen),
379 diffstat=diffstats,
395 diffstat=diffstats,
380 archives=web.archivelist(ctx.hex()),
396 archives=web.archivelist(ctx.hex()),
381 tags=nodetagsdict(web.repo, ctx.node()),
397 tags=nodetagsdict(web.repo, ctx.node()),
382 bookmarks=nodebookmarksdict(web.repo, ctx.node()),
398 bookmarks=nodebookmarksdict(web.repo, ctx.node()),
383 branch=showbranch,
399 branch=showbranch,
384 inbranch=nodeinbranch(web.repo, ctx),
400 inbranch=nodeinbranch(web.repo, ctx),
385 branches=nodebranchdict(web.repo, ctx))
401 branches=nodebranchdict(web.repo, ctx))
386
402
387 def listfilediffs(tmpl, files, node, max):
403 def listfilediffs(tmpl, files, node, max):
388 for f in files[:max]:
404 for f in files[:max]:
389 yield tmpl('filedifflink', node=hex(node), file=f)
405 yield tmpl('filedifflink', node=hex(node), file=f)
390 if len(files) > max:
406 if len(files) > max:
391 yield tmpl('fileellipses')
407 yield tmpl('fileellipses')
392
408
393 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
409 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
394
410
395 def countgen():
411 def countgen():
396 start = 1
412 start = 1
397 while True:
413 while True:
398 yield start
414 yield start
399 start += 1
415 start += 1
400
416
401 blockcount = countgen()
417 blockcount = countgen()
402 def prettyprintlines(diff, blockno):
418 def prettyprintlines(diff, blockno):
403 for lineno, l in enumerate(diff.splitlines(True)):
419 for lineno, l in enumerate(diff.splitlines(True)):
404 difflineno = "%d.%d" % (blockno, lineno + 1)
420 difflineno = "%d.%d" % (blockno, lineno + 1)
405 if l.startswith('+'):
421 if l.startswith('+'):
406 ltype = "difflineplus"
422 ltype = "difflineplus"
407 elif l.startswith('-'):
423 elif l.startswith('-'):
408 ltype = "difflineminus"
424 ltype = "difflineminus"
409 elif l.startswith('@'):
425 elif l.startswith('@'):
410 ltype = "difflineat"
426 ltype = "difflineat"
411 else:
427 else:
412 ltype = "diffline"
428 ltype = "diffline"
413 yield tmpl(ltype,
429 yield tmpl(ltype,
414 line=l,
430 line=l,
415 lineno=lineno + 1,
431 lineno=lineno + 1,
416 lineid="l%s" % difflineno,
432 lineid="l%s" % difflineno,
417 linenumber="% 8s" % difflineno)
433 linenumber="% 8s" % difflineno)
418
434
419 if files:
435 if files:
420 m = match.exact(repo.root, repo.getcwd(), files)
436 m = match.exact(repo.root, repo.getcwd(), files)
421 else:
437 else:
422 m = match.always(repo.root, repo.getcwd())
438 m = match.always(repo.root, repo.getcwd())
423
439
424 diffopts = patch.diffopts(repo.ui, untrusted=True)
440 diffopts = patch.diffopts(repo.ui, untrusted=True)
425 if basectx is None:
441 if basectx is None:
426 parents = ctx.parents()
442 parents = ctx.parents()
427 if parents:
443 if parents:
428 node1 = parents[0].node()
444 node1 = parents[0].node()
429 else:
445 else:
430 node1 = nullid
446 node1 = nullid
431 else:
447 else:
432 node1 = basectx.node()
448 node1 = basectx.node()
433 node2 = ctx.node()
449 node2 = ctx.node()
434
450
435 block = []
451 block = []
436 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
452 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
437 if chunk.startswith('diff') and block:
453 if chunk.startswith('diff') and block:
438 blockno = blockcount.next()
454 blockno = blockcount.next()
439 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
455 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
440 lines=prettyprintlines(''.join(block), blockno))
456 lines=prettyprintlines(''.join(block), blockno))
441 block = []
457 block = []
442 if chunk.startswith('diff') and style != 'raw':
458 if chunk.startswith('diff') and style != 'raw':
443 chunk = ''.join(chunk.splitlines(True)[1:])
459 chunk = ''.join(chunk.splitlines(True)[1:])
444 block.append(chunk)
460 block.append(chunk)
445 blockno = blockcount.next()
461 blockno = blockcount.next()
446 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
462 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
447 lines=prettyprintlines(''.join(block), blockno))
463 lines=prettyprintlines(''.join(block), blockno))
448
464
449 def compare(tmpl, context, leftlines, rightlines):
465 def compare(tmpl, context, leftlines, rightlines):
450 '''Generator function that provides side-by-side comparison data.'''
466 '''Generator function that provides side-by-side comparison data.'''
451
467
452 def compline(type, leftlineno, leftline, rightlineno, rightline):
468 def compline(type, leftlineno, leftline, rightlineno, rightline):
453 lineid = leftlineno and ("l%s" % leftlineno) or ''
469 lineid = leftlineno and ("l%s" % leftlineno) or ''
454 lineid += rightlineno and ("r%s" % rightlineno) or ''
470 lineid += rightlineno and ("r%s" % rightlineno) or ''
455 return tmpl('comparisonline',
471 return tmpl('comparisonline',
456 type=type,
472 type=type,
457 lineid=lineid,
473 lineid=lineid,
458 leftlineno=leftlineno,
474 leftlineno=leftlineno,
459 leftlinenumber="% 6s" % (leftlineno or ''),
475 leftlinenumber="% 6s" % (leftlineno or ''),
460 leftline=leftline or '',
476 leftline=leftline or '',
461 rightlineno=rightlineno,
477 rightlineno=rightlineno,
462 rightlinenumber="% 6s" % (rightlineno or ''),
478 rightlinenumber="% 6s" % (rightlineno or ''),
463 rightline=rightline or '')
479 rightline=rightline or '')
464
480
465 def getblock(opcodes):
481 def getblock(opcodes):
466 for type, llo, lhi, rlo, rhi in opcodes:
482 for type, llo, lhi, rlo, rhi in opcodes:
467 len1 = lhi - llo
483 len1 = lhi - llo
468 len2 = rhi - rlo
484 len2 = rhi - rlo
469 count = min(len1, len2)
485 count = min(len1, len2)
470 for i in xrange(count):
486 for i in xrange(count):
471 yield compline(type=type,
487 yield compline(type=type,
472 leftlineno=llo + i + 1,
488 leftlineno=llo + i + 1,
473 leftline=leftlines[llo + i],
489 leftline=leftlines[llo + i],
474 rightlineno=rlo + i + 1,
490 rightlineno=rlo + i + 1,
475 rightline=rightlines[rlo + i])
491 rightline=rightlines[rlo + i])
476 if len1 > len2:
492 if len1 > len2:
477 for i in xrange(llo + count, lhi):
493 for i in xrange(llo + count, lhi):
478 yield compline(type=type,
494 yield compline(type=type,
479 leftlineno=i + 1,
495 leftlineno=i + 1,
480 leftline=leftlines[i],
496 leftline=leftlines[i],
481 rightlineno=None,
497 rightlineno=None,
482 rightline=None)
498 rightline=None)
483 elif len2 > len1:
499 elif len2 > len1:
484 for i in xrange(rlo + count, rhi):
500 for i in xrange(rlo + count, rhi):
485 yield compline(type=type,
501 yield compline(type=type,
486 leftlineno=None,
502 leftlineno=None,
487 leftline=None,
503 leftline=None,
488 rightlineno=i + 1,
504 rightlineno=i + 1,
489 rightline=rightlines[i])
505 rightline=rightlines[i])
490
506
491 s = difflib.SequenceMatcher(None, leftlines, rightlines)
507 s = difflib.SequenceMatcher(None, leftlines, rightlines)
492 if context < 0:
508 if context < 0:
493 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
509 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
494 else:
510 else:
495 for oc in s.get_grouped_opcodes(n=context):
511 for oc in s.get_grouped_opcodes(n=context):
496 yield tmpl('comparisonblock', lines=getblock(oc))
512 yield tmpl('comparisonblock', lines=getblock(oc))
497
513
498 def diffstatgen(ctx, basectx):
514 def diffstatgen(ctx, basectx):
499 '''Generator function that provides the diffstat data.'''
515 '''Generator function that provides the diffstat data.'''
500
516
501 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
517 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
502 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
518 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
503 while True:
519 while True:
504 yield stats, maxname, maxtotal, addtotal, removetotal, binary
520 yield stats, maxname, maxtotal, addtotal, removetotal, binary
505
521
506 def diffsummary(statgen):
522 def diffsummary(statgen):
507 '''Return a short summary of the diff.'''
523 '''Return a short summary of the diff.'''
508
524
509 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
525 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
510 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
526 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
511 len(stats), addtotal, removetotal)
527 len(stats), addtotal, removetotal)
512
528
513 def diffstat(tmpl, ctx, statgen, parity):
529 def diffstat(tmpl, ctx, statgen, parity):
514 '''Return a diffstat template for each file in the diff.'''
530 '''Return a diffstat template for each file in the diff.'''
515
531
516 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
532 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
517 files = ctx.files()
533 files = ctx.files()
518
534
519 def pct(i):
535 def pct(i):
520 if maxtotal == 0:
536 if maxtotal == 0:
521 return 0
537 return 0
522 return (float(i) / maxtotal) * 100
538 return (float(i) / maxtotal) * 100
523
539
524 fileno = 0
540 fileno = 0
525 for filename, adds, removes, isbinary in stats:
541 for filename, adds, removes, isbinary in stats:
526 template = filename in files and 'diffstatlink' or 'diffstatnolink'
542 template = filename in files and 'diffstatlink' or 'diffstatnolink'
527 total = adds + removes
543 total = adds + removes
528 fileno += 1
544 fileno += 1
529 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
545 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
530 total=total, addpct=pct(adds), removepct=pct(removes),
546 total=total, addpct=pct(adds), removepct=pct(removes),
531 parity=parity.next())
547 parity=parity.next())
532
548
533 class sessionvars(object):
549 class sessionvars(object):
534 def __init__(self, vars, start='?'):
550 def __init__(self, vars, start='?'):
535 self.start = start
551 self.start = start
536 self.vars = vars
552 self.vars = vars
537 def __getitem__(self, key):
553 def __getitem__(self, key):
538 return self.vars[key]
554 return self.vars[key]
539 def __setitem__(self, key, value):
555 def __setitem__(self, key, value):
540 self.vars[key] = value
556 self.vars[key] = value
541 def __copy__(self):
557 def __copy__(self):
542 return sessionvars(copy.copy(self.vars), self.start)
558 return sessionvars(copy.copy(self.vars), self.start)
543 def __iter__(self):
559 def __iter__(self):
544 separator = self.start
560 separator = self.start
545 for key, value in sorted(self.vars.iteritems()):
561 for key, value in sorted(self.vars.iteritems()):
546 yield {'name': key, 'value': str(value), 'separator': separator}
562 yield {'name': key, 'value': str(value), 'separator': separator}
547 separator = '&'
563 separator = '&'
548
564
549 class wsgiui(uimod.ui):
565 class wsgiui(uimod.ui):
550 # default termwidth breaks under mod_wsgi
566 # default termwidth breaks under mod_wsgi
551 def termwidth(self):
567 def termwidth(self):
552 return 80
568 return 80
553
569
554 def getwebsubs(repo):
570 def getwebsubs(repo):
555 websubtable = []
571 websubtable = []
556 websubdefs = repo.ui.configitems('websub')
572 websubdefs = repo.ui.configitems('websub')
557 # we must maintain interhg backwards compatibility
573 # we must maintain interhg backwards compatibility
558 websubdefs += repo.ui.configitems('interhg')
574 websubdefs += repo.ui.configitems('interhg')
559 for key, pattern in websubdefs:
575 for key, pattern in websubdefs:
560 # grab the delimiter from the character after the "s"
576 # grab the delimiter from the character after the "s"
561 unesc = pattern[1]
577 unesc = pattern[1]
562 delim = re.escape(unesc)
578 delim = re.escape(unesc)
563
579
564 # identify portions of the pattern, taking care to avoid escaped
580 # identify portions of the pattern, taking care to avoid escaped
565 # delimiters. the replace format and flags are optional, but
581 # delimiters. the replace format and flags are optional, but
566 # delimiters are required.
582 # delimiters are required.
567 match = re.match(
583 match = re.match(
568 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
584 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
569 % (delim, delim, delim), pattern)
585 % (delim, delim, delim), pattern)
570 if not match:
586 if not match:
571 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
587 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
572 % (key, pattern))
588 % (key, pattern))
573 continue
589 continue
574
590
575 # we need to unescape the delimiter for regexp and format
591 # we need to unescape the delimiter for regexp and format
576 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
592 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
577 regexp = delim_re.sub(unesc, match.group(1))
593 regexp = delim_re.sub(unesc, match.group(1))
578 format = delim_re.sub(unesc, match.group(2))
594 format = delim_re.sub(unesc, match.group(2))
579
595
580 # the pattern allows for 6 regexp flags, so set them if necessary
596 # the pattern allows for 6 regexp flags, so set them if necessary
581 flagin = match.group(3)
597 flagin = match.group(3)
582 flags = 0
598 flags = 0
583 if flagin:
599 if flagin:
584 for flag in flagin.upper():
600 for flag in flagin.upper():
585 flags |= re.__dict__[flag]
601 flags |= re.__dict__[flag]
586
602
587 try:
603 try:
588 regexp = re.compile(regexp, flags)
604 regexp = re.compile(regexp, flags)
589 websubtable.append((regexp, format))
605 websubtable.append((regexp, format))
590 except re.error:
606 except re.error:
591 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
607 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
592 % (key, regexp))
608 % (key, regexp))
593 return websubtable
609 return websubtable
@@ -1,83 +1,92 b''
1 # hgweb/wsgicgi.py - CGI->WSGI translator
1 # hgweb/wsgicgi.py - CGI->WSGI translator
2 #
2 #
3 # Copyright 2006 Eric Hopper <hopper@omnifarious.org>
3 # Copyright 2006 Eric Hopper <hopper@omnifarious.org>
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 # This was originally copied from the public domain code at
8 # This was originally copied from the public domain code at
9 # http://www.python.org/dev/peps/pep-0333/#the-server-gateway-side
9 # http://www.python.org/dev/peps/pep-0333/#the-server-gateway-side
10
10
11 import os, sys
11 from __future__ import absolute_import
12 from mercurial import util
12
13 from mercurial.hgweb import common
13 import os
14 import sys
15
16 from .. import (
17 util,
18 )
19
20 from . import (
21 common,
22 )
14
23
15 def launch(application):
24 def launch(application):
16 util.setbinary(sys.stdin)
25 util.setbinary(sys.stdin)
17 util.setbinary(sys.stdout)
26 util.setbinary(sys.stdout)
18
27
19 environ = dict(os.environ.iteritems())
28 environ = dict(os.environ.iteritems())
20 environ.setdefault('PATH_INFO', '')
29 environ.setdefault('PATH_INFO', '')
21 if environ.get('SERVER_SOFTWARE', '').startswith('Microsoft-IIS'):
30 if environ.get('SERVER_SOFTWARE', '').startswith('Microsoft-IIS'):
22 # IIS includes script_name in PATH_INFO
31 # IIS includes script_name in PATH_INFO
23 scriptname = environ['SCRIPT_NAME']
32 scriptname = environ['SCRIPT_NAME']
24 if environ['PATH_INFO'].startswith(scriptname):
33 if environ['PATH_INFO'].startswith(scriptname):
25 environ['PATH_INFO'] = environ['PATH_INFO'][len(scriptname):]
34 environ['PATH_INFO'] = environ['PATH_INFO'][len(scriptname):]
26
35
27 stdin = sys.stdin
36 stdin = sys.stdin
28 if environ.get('HTTP_EXPECT', '').lower() == '100-continue':
37 if environ.get('HTTP_EXPECT', '').lower() == '100-continue':
29 stdin = common.continuereader(stdin, sys.stdout.write)
38 stdin = common.continuereader(stdin, sys.stdout.write)
30
39
31 environ['wsgi.input'] = stdin
40 environ['wsgi.input'] = stdin
32 environ['wsgi.errors'] = sys.stderr
41 environ['wsgi.errors'] = sys.stderr
33 environ['wsgi.version'] = (1, 0)
42 environ['wsgi.version'] = (1, 0)
34 environ['wsgi.multithread'] = False
43 environ['wsgi.multithread'] = False
35 environ['wsgi.multiprocess'] = True
44 environ['wsgi.multiprocess'] = True
36 environ['wsgi.run_once'] = True
45 environ['wsgi.run_once'] = True
37
46
38 if environ.get('HTTPS', 'off').lower() in ('on', '1', 'yes'):
47 if environ.get('HTTPS', 'off').lower() in ('on', '1', 'yes'):
39 environ['wsgi.url_scheme'] = 'https'
48 environ['wsgi.url_scheme'] = 'https'
40 else:
49 else:
41 environ['wsgi.url_scheme'] = 'http'
50 environ['wsgi.url_scheme'] = 'http'
42
51
43 headers_set = []
52 headers_set = []
44 headers_sent = []
53 headers_sent = []
45 out = sys.stdout
54 out = sys.stdout
46
55
47 def write(data):
56 def write(data):
48 if not headers_set:
57 if not headers_set:
49 raise AssertionError("write() before start_response()")
58 raise AssertionError("write() before start_response()")
50
59
51 elif not headers_sent:
60 elif not headers_sent:
52 # Before the first output, send the stored headers
61 # Before the first output, send the stored headers
53 status, response_headers = headers_sent[:] = headers_set
62 status, response_headers = headers_sent[:] = headers_set
54 out.write('Status: %s\r\n' % status)
63 out.write('Status: %s\r\n' % status)
55 for header in response_headers:
64 for header in response_headers:
56 out.write('%s: %s\r\n' % header)
65 out.write('%s: %s\r\n' % header)
57 out.write('\r\n')
66 out.write('\r\n')
58
67
59 out.write(data)
68 out.write(data)
60 out.flush()
69 out.flush()
61
70
62 def start_response(status, response_headers, exc_info=None):
71 def start_response(status, response_headers, exc_info=None):
63 if exc_info:
72 if exc_info:
64 try:
73 try:
65 if headers_sent:
74 if headers_sent:
66 # Re-raise original exception if headers sent
75 # Re-raise original exception if headers sent
67 raise exc_info[0](exc_info[1], exc_info[2])
76 raise exc_info[0](exc_info[1], exc_info[2])
68 finally:
77 finally:
69 exc_info = None # avoid dangling circular ref
78 exc_info = None # avoid dangling circular ref
70 elif headers_set:
79 elif headers_set:
71 raise AssertionError("Headers already set!")
80 raise AssertionError("Headers already set!")
72
81
73 headers_set[:] = [status, response_headers]
82 headers_set[:] = [status, response_headers]
74 return write
83 return write
75
84
76 content = application(environ, start_response)
85 content = application(environ, start_response)
77 try:
86 try:
78 for chunk in content:
87 for chunk in content:
79 write(chunk)
88 write(chunk)
80 if not headers_sent:
89 if not headers_sent:
81 write('') # send headers now if body was empty
90 write('') # send headers now if body was empty
82 finally:
91 finally:
83 getattr(content, 'close', lambda : None)()
92 getattr(content, 'close', lambda : None)()
General Comments 0
You need to be logged in to leave comments. Login now