##// END OF EJS Templates
configitems: register the 'web.description' config
Boris Feld -
r34235:a6c18628 default
parent child Browse files
Show More
@@ -1,215 +1,219 b''
1 # zeroconf.py - zeroconf support for Mercurial
1 # zeroconf.py - zeroconf support for Mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 '''discover and advertise repositories on the local network
7 '''discover and advertise repositories on the local network
8
8
9 Zeroconf-enabled repositories will be announced in a network without
9 Zeroconf-enabled repositories will be announced in a network without
10 the need to configure a server or a service. They can be discovered
10 the need to configure a server or a service. They can be discovered
11 without knowing their actual IP address.
11 without knowing their actual IP address.
12
12
13 To allow other people to discover your repository using run
13 To allow other people to discover your repository using run
14 :hg:`serve` in your repository::
14 :hg:`serve` in your repository::
15
15
16 $ cd test
16 $ cd test
17 $ hg serve
17 $ hg serve
18
18
19 You can discover Zeroconf-enabled repositories by running
19 You can discover Zeroconf-enabled repositories by running
20 :hg:`paths`::
20 :hg:`paths`::
21
21
22 $ hg paths
22 $ hg paths
23 zc-test = http://example.com:8000/test
23 zc-test = http://example.com:8000/test
24 '''
24 '''
25 from __future__ import absolute_import
25 from __future__ import absolute_import
26
26
27 import os
27 import os
28 import socket
28 import socket
29 import time
29 import time
30
30
31 from . import Zeroconf
31 from . import Zeroconf
32 from mercurial import (
32 from mercurial import (
33 dispatch,
33 dispatch,
34 encoding,
34 encoding,
35 extensions,
35 extensions,
36 hg,
36 hg,
37 ui as uimod,
37 ui as uimod,
38 )
38 )
39 from mercurial.hgweb import (
39 from mercurial.hgweb import (
40 server as servermod
40 server as servermod
41 )
41 )
42
42
43 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
43 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
45 # be specifying the version(s) of Mercurial they are tested with, or
45 # be specifying the version(s) of Mercurial they are tested with, or
46 # leave the attribute unspecified.
46 # leave the attribute unspecified.
47 testedwith = 'ships-with-hg-core'
47 testedwith = 'ships-with-hg-core'
48
48
49 # publish
49 # publish
50
50
51 server = None
51 server = None
52 localip = None
52 localip = None
53
53
54 def getip():
54 def getip():
55 # finds external-facing interface without sending any packets (Linux)
55 # finds external-facing interface without sending any packets (Linux)
56 try:
56 try:
57 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
57 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
58 s.connect(('1.0.0.1', 0))
58 s.connect(('1.0.0.1', 0))
59 ip = s.getsockname()[0]
59 ip = s.getsockname()[0]
60 return ip
60 return ip
61 except socket.error:
61 except socket.error:
62 pass
62 pass
63
63
64 # Generic method, sometimes gives useless results
64 # Generic method, sometimes gives useless results
65 try:
65 try:
66 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
66 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
67 if ':' in dumbip:
67 if ':' in dumbip:
68 dumbip = '127.0.0.1'
68 dumbip = '127.0.0.1'
69 if not dumbip.startswith('127.'):
69 if not dumbip.startswith('127.'):
70 return dumbip
70 return dumbip
71 except (socket.gaierror, socket.herror):
71 except (socket.gaierror, socket.herror):
72 dumbip = '127.0.0.1'
72 dumbip = '127.0.0.1'
73
73
74 # works elsewhere, but actually sends a packet
74 # works elsewhere, but actually sends a packet
75 try:
75 try:
76 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
76 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
77 s.connect(('1.0.0.1', 1))
77 s.connect(('1.0.0.1', 1))
78 ip = s.getsockname()[0]
78 ip = s.getsockname()[0]
79 return ip
79 return ip
80 except socket.error:
80 except socket.error:
81 pass
81 pass
82
82
83 return dumbip
83 return dumbip
84
84
85 def publish(name, desc, path, port):
85 def publish(name, desc, path, port):
86 global server, localip
86 global server, localip
87 if not server:
87 if not server:
88 ip = getip()
88 ip = getip()
89 if ip.startswith('127.'):
89 if ip.startswith('127.'):
90 # if we have no internet connection, this can happen.
90 # if we have no internet connection, this can happen.
91 return
91 return
92 localip = socket.inet_aton(ip)
92 localip = socket.inet_aton(ip)
93 server = Zeroconf.Zeroconf(ip)
93 server = Zeroconf.Zeroconf(ip)
94
94
95 hostname = socket.gethostname().split('.')[0]
95 hostname = socket.gethostname().split('.')[0]
96 host = hostname + ".local"
96 host = hostname + ".local"
97 name = "%s-%s" % (hostname, name)
97 name = "%s-%s" % (hostname, name)
98
98
99 # advertise to browsers
99 # advertise to browsers
100 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
100 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
101 name + '._http._tcp.local.',
101 name + '._http._tcp.local.',
102 server = host,
102 server = host,
103 port = port,
103 port = port,
104 properties = {'description': desc,
104 properties = {'description': desc,
105 'path': "/" + path},
105 'path': "/" + path},
106 address = localip, weight = 0, priority = 0)
106 address = localip, weight = 0, priority = 0)
107 server.registerService(svc)
107 server.registerService(svc)
108
108
109 # advertise to Mercurial clients
109 # advertise to Mercurial clients
110 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
110 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
111 name + '._hg._tcp.local.',
111 name + '._hg._tcp.local.',
112 server = host,
112 server = host,
113 port = port,
113 port = port,
114 properties = {'description': desc,
114 properties = {'description': desc,
115 'path': "/" + path},
115 'path': "/" + path},
116 address = localip, weight = 0, priority = 0)
116 address = localip, weight = 0, priority = 0)
117 server.registerService(svc)
117 server.registerService(svc)
118
118
119 def zc_create_server(create_server, ui, app):
119 def zc_create_server(create_server, ui, app):
120 httpd = create_server(ui, app)
120 httpd = create_server(ui, app)
121 port = httpd.port
121 port = httpd.port
122
122
123 try:
123 try:
124 repos = app.repos
124 repos = app.repos
125 except AttributeError:
125 except AttributeError:
126 # single repo
126 # single repo
127 with app._obtainrepo() as repo:
127 with app._obtainrepo() as repo:
128 name = app.reponame or os.path.basename(repo.root)
128 name = app.reponame or os.path.basename(repo.root)
129 path = repo.ui.config("web", "prefix", "").strip('/')
129 path = repo.ui.config("web", "prefix", "").strip('/')
130 desc = repo.ui.config("web", "description", name)
130 desc = repo.ui.config("web", "description")
131 if not desc:
132 desc = name
131 publish(name, desc, path, port)
133 publish(name, desc, path, port)
132 else:
134 else:
133 # webdir
135 # webdir
134 prefix = app.ui.config("web", "prefix", "").strip('/') + '/'
136 prefix = app.ui.config("web", "prefix", "").strip('/') + '/'
135 for repo, path in repos:
137 for repo, path in repos:
136 u = app.ui.copy()
138 u = app.ui.copy()
137 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
139 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
138 name = os.path.basename(repo)
140 name = os.path.basename(repo)
139 path = (prefix + repo).strip('/')
141 path = (prefix + repo).strip('/')
140 desc = u.config('web', 'description', name)
142 desc = u.config('web', 'description')
143 if not desc:
144 desc = name
141 publish(name, desc, path, port)
145 publish(name, desc, path, port)
142 return httpd
146 return httpd
143
147
144 # listen
148 # listen
145
149
146 class listener(object):
150 class listener(object):
147 def __init__(self):
151 def __init__(self):
148 self.found = {}
152 self.found = {}
149 def removeService(self, server, type, name):
153 def removeService(self, server, type, name):
150 if repr(name) in self.found:
154 if repr(name) in self.found:
151 del self.found[repr(name)]
155 del self.found[repr(name)]
152 def addService(self, server, type, name):
156 def addService(self, server, type, name):
153 self.found[repr(name)] = server.getServiceInfo(type, name)
157 self.found[repr(name)] = server.getServiceInfo(type, name)
154
158
155 def getzcpaths():
159 def getzcpaths():
156 ip = getip()
160 ip = getip()
157 if ip.startswith('127.'):
161 if ip.startswith('127.'):
158 return
162 return
159 server = Zeroconf.Zeroconf(ip)
163 server = Zeroconf.Zeroconf(ip)
160 l = listener()
164 l = listener()
161 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
165 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
162 time.sleep(1)
166 time.sleep(1)
163 server.close()
167 server.close()
164 for value in l.found.values():
168 for value in l.found.values():
165 name = value.name[:value.name.index('.')]
169 name = value.name[:value.name.index('.')]
166 url = "http://%s:%s%s" % (socket.inet_ntoa(value.address), value.port,
170 url = "http://%s:%s%s" % (socket.inet_ntoa(value.address), value.port,
167 value.properties.get("path", "/"))
171 value.properties.get("path", "/"))
168 yield "zc-" + name, url
172 yield "zc-" + name, url
169
173
170 def config(orig, self, section, key, *args, **kwargs):
174 def config(orig, self, section, key, *args, **kwargs):
171 if section == "paths" and key.startswith("zc-"):
175 if section == "paths" and key.startswith("zc-"):
172 for name, path in getzcpaths():
176 for name, path in getzcpaths():
173 if name == key:
177 if name == key:
174 return path
178 return path
175 return orig(self, section, key, *args, **kwargs)
179 return orig(self, section, key, *args, **kwargs)
176
180
177 def configitems(orig, self, section, *args, **kwargs):
181 def configitems(orig, self, section, *args, **kwargs):
178 repos = orig(self, section, *args, **kwargs)
182 repos = orig(self, section, *args, **kwargs)
179 if section == "paths":
183 if section == "paths":
180 repos += getzcpaths()
184 repos += getzcpaths()
181 return repos
185 return repos
182
186
183 def configsuboptions(orig, self, section, name, *args, **kwargs):
187 def configsuboptions(orig, self, section, name, *args, **kwargs):
184 opt, sub = orig(self, section, name, *args, **kwargs)
188 opt, sub = orig(self, section, name, *args, **kwargs)
185 if section == "paths" and name.startswith("zc-"):
189 if section == "paths" and name.startswith("zc-"):
186 # We have to find the URL in the zeroconf paths. We can't cons up any
190 # We have to find the URL in the zeroconf paths. We can't cons up any
187 # suboptions, so we use any that we found in the original config.
191 # suboptions, so we use any that we found in the original config.
188 for zcname, zcurl in getzcpaths():
192 for zcname, zcurl in getzcpaths():
189 if zcname == name:
193 if zcname == name:
190 return zcurl, sub
194 return zcurl, sub
191 return opt, sub
195 return opt, sub
192
196
193 def defaultdest(orig, source):
197 def defaultdest(orig, source):
194 for name, path in getzcpaths():
198 for name, path in getzcpaths():
195 if path == source:
199 if path == source:
196 return name.encode(encoding.encoding)
200 return name.encode(encoding.encoding)
197 return orig(source)
201 return orig(source)
198
202
199 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
203 def cleanupafterdispatch(orig, ui, options, cmd, cmdfunc):
200 try:
204 try:
201 return orig(ui, options, cmd, cmdfunc)
205 return orig(ui, options, cmd, cmdfunc)
202 finally:
206 finally:
203 # we need to call close() on the server to notify() the various
207 # we need to call close() on the server to notify() the various
204 # threading Conditions and allow the background threads to exit
208 # threading Conditions and allow the background threads to exit
205 global server
209 global server
206 if server:
210 if server:
207 server.close()
211 server.close()
208
212
209 extensions.wrapfunction(dispatch, '_runcommand', cleanupafterdispatch)
213 extensions.wrapfunction(dispatch, '_runcommand', cleanupafterdispatch)
210
214
211 extensions.wrapfunction(uimod.ui, 'config', config)
215 extensions.wrapfunction(uimod.ui, 'config', config)
212 extensions.wrapfunction(uimod.ui, 'configitems', configitems)
216 extensions.wrapfunction(uimod.ui, 'configitems', configitems)
213 extensions.wrapfunction(uimod.ui, 'configsuboptions', configsuboptions)
217 extensions.wrapfunction(uimod.ui, 'configsuboptions', configsuboptions)
214 extensions.wrapfunction(hg, 'defaultdest', defaultdest)
218 extensions.wrapfunction(hg, 'defaultdest', defaultdest)
215 extensions.wrapfunction(servermod, 'create_server', zc_create_server)
219 extensions.wrapfunction(servermod, 'create_server', zc_create_server)
@@ -1,634 +1,637 b''
1 # configitems.py - centralized declaration of configuration option
1 # configitems.py - centralized declaration of configuration option
2 #
2 #
3 # Copyright 2017 Pierre-Yves David <pierre-yves.david@octobus.net>
3 # Copyright 2017 Pierre-Yves David <pierre-yves.david@octobus.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import functools
10 import functools
11
11
12 from . import (
12 from . import (
13 error,
13 error,
14 )
14 )
15
15
16 def loadconfigtable(ui, extname, configtable):
16 def loadconfigtable(ui, extname, configtable):
17 """update config item known to the ui with the extension ones"""
17 """update config item known to the ui with the extension ones"""
18 for section, items in configtable.items():
18 for section, items in configtable.items():
19 knownitems = ui._knownconfig.setdefault(section, {})
19 knownitems = ui._knownconfig.setdefault(section, {})
20 knownkeys = set(knownitems)
20 knownkeys = set(knownitems)
21 newkeys = set(items)
21 newkeys = set(items)
22 for key in sorted(knownkeys & newkeys):
22 for key in sorted(knownkeys & newkeys):
23 msg = "extension '%s' overwrite config item '%s.%s'"
23 msg = "extension '%s' overwrite config item '%s.%s'"
24 msg %= (extname, section, key)
24 msg %= (extname, section, key)
25 ui.develwarn(msg, config='warn-config')
25 ui.develwarn(msg, config='warn-config')
26
26
27 knownitems.update(items)
27 knownitems.update(items)
28
28
29 class configitem(object):
29 class configitem(object):
30 """represent a known config item
30 """represent a known config item
31
31
32 :section: the official config section where to find this item,
32 :section: the official config section where to find this item,
33 :name: the official name within the section,
33 :name: the official name within the section,
34 :default: default value for this item,
34 :default: default value for this item,
35 :alias: optional list of tuples as alternatives.
35 :alias: optional list of tuples as alternatives.
36 """
36 """
37
37
38 def __init__(self, section, name, default=None, alias=()):
38 def __init__(self, section, name, default=None, alias=()):
39 self.section = section
39 self.section = section
40 self.name = name
40 self.name = name
41 self.default = default
41 self.default = default
42 self.alias = list(alias)
42 self.alias = list(alias)
43
43
44 coreitems = {}
44 coreitems = {}
45
45
46 def _register(configtable, *args, **kwargs):
46 def _register(configtable, *args, **kwargs):
47 item = configitem(*args, **kwargs)
47 item = configitem(*args, **kwargs)
48 section = configtable.setdefault(item.section, {})
48 section = configtable.setdefault(item.section, {})
49 if item.name in section:
49 if item.name in section:
50 msg = "duplicated config item registration for '%s.%s'"
50 msg = "duplicated config item registration for '%s.%s'"
51 raise error.ProgrammingError(msg % (item.section, item.name))
51 raise error.ProgrammingError(msg % (item.section, item.name))
52 section[item.name] = item
52 section[item.name] = item
53
53
54 # special value for case where the default is derived from other values
54 # special value for case where the default is derived from other values
55 dynamicdefault = object()
55 dynamicdefault = object()
56
56
57 # Registering actual config items
57 # Registering actual config items
58
58
59 def getitemregister(configtable):
59 def getitemregister(configtable):
60 return functools.partial(_register, configtable)
60 return functools.partial(_register, configtable)
61
61
62 coreconfigitem = getitemregister(coreitems)
62 coreconfigitem = getitemregister(coreitems)
63
63
64 coreconfigitem('auth', 'cookiefile',
64 coreconfigitem('auth', 'cookiefile',
65 default=None,
65 default=None,
66 )
66 )
67 # bookmarks.pushing: internal hack for discovery
67 # bookmarks.pushing: internal hack for discovery
68 coreconfigitem('bookmarks', 'pushing',
68 coreconfigitem('bookmarks', 'pushing',
69 default=list,
69 default=list,
70 )
70 )
71 # bundle.mainreporoot: internal hack for bundlerepo
71 # bundle.mainreporoot: internal hack for bundlerepo
72 coreconfigitem('bundle', 'mainreporoot',
72 coreconfigitem('bundle', 'mainreporoot',
73 default='',
73 default='',
74 )
74 )
75 # bundle.reorder: experimental config
75 # bundle.reorder: experimental config
76 coreconfigitem('bundle', 'reorder',
76 coreconfigitem('bundle', 'reorder',
77 default='auto',
77 default='auto',
78 )
78 )
79 coreconfigitem('censor', 'policy',
79 coreconfigitem('censor', 'policy',
80 default='abort',
80 default='abort',
81 )
81 )
82 coreconfigitem('chgserver', 'idletimeout',
82 coreconfigitem('chgserver', 'idletimeout',
83 default=3600,
83 default=3600,
84 )
84 )
85 coreconfigitem('chgserver', 'skiphash',
85 coreconfigitem('chgserver', 'skiphash',
86 default=False,
86 default=False,
87 )
87 )
88 coreconfigitem('cmdserver', 'log',
88 coreconfigitem('cmdserver', 'log',
89 default=None,
89 default=None,
90 )
90 )
91 coreconfigitem('color', 'mode',
91 coreconfigitem('color', 'mode',
92 default='auto',
92 default='auto',
93 )
93 )
94 coreconfigitem('color', 'pagermode',
94 coreconfigitem('color', 'pagermode',
95 default=dynamicdefault,
95 default=dynamicdefault,
96 )
96 )
97 coreconfigitem('commands', 'status.relative',
97 coreconfigitem('commands', 'status.relative',
98 default=False,
98 default=False,
99 )
99 )
100 coreconfigitem('commands', 'status.skipstates',
100 coreconfigitem('commands', 'status.skipstates',
101 default=[],
101 default=[],
102 )
102 )
103 coreconfigitem('commands', 'status.verbose',
103 coreconfigitem('commands', 'status.verbose',
104 default=False,
104 default=False,
105 )
105 )
106 coreconfigitem('commands', 'update.requiredest',
106 coreconfigitem('commands', 'update.requiredest',
107 default=False,
107 default=False,
108 )
108 )
109 coreconfigitem('devel', 'all-warnings',
109 coreconfigitem('devel', 'all-warnings',
110 default=False,
110 default=False,
111 )
111 )
112 coreconfigitem('devel', 'bundle2.debug',
112 coreconfigitem('devel', 'bundle2.debug',
113 default=False,
113 default=False,
114 )
114 )
115 coreconfigitem('devel', 'check-locks',
115 coreconfigitem('devel', 'check-locks',
116 default=False,
116 default=False,
117 )
117 )
118 coreconfigitem('devel', 'check-relroot',
118 coreconfigitem('devel', 'check-relroot',
119 default=False,
119 default=False,
120 )
120 )
121 coreconfigitem('devel', 'default-date',
121 coreconfigitem('devel', 'default-date',
122 default=None,
122 default=None,
123 )
123 )
124 coreconfigitem('devel', 'deprec-warn',
124 coreconfigitem('devel', 'deprec-warn',
125 default=False,
125 default=False,
126 )
126 )
127 coreconfigitem('devel', 'disableloaddefaultcerts',
127 coreconfigitem('devel', 'disableloaddefaultcerts',
128 default=False,
128 default=False,
129 )
129 )
130 coreconfigitem('devel', 'legacy.exchange',
130 coreconfigitem('devel', 'legacy.exchange',
131 default=list,
131 default=list,
132 )
132 )
133 coreconfigitem('devel', 'servercafile',
133 coreconfigitem('devel', 'servercafile',
134 default='',
134 default='',
135 )
135 )
136 coreconfigitem('devel', 'serverexactprotocol',
136 coreconfigitem('devel', 'serverexactprotocol',
137 default='',
137 default='',
138 )
138 )
139 coreconfigitem('devel', 'serverrequirecert',
139 coreconfigitem('devel', 'serverrequirecert',
140 default=False,
140 default=False,
141 )
141 )
142 coreconfigitem('devel', 'strip-obsmarkers',
142 coreconfigitem('devel', 'strip-obsmarkers',
143 default=True,
143 default=True,
144 )
144 )
145 coreconfigitem('email', 'charsets',
145 coreconfigitem('email', 'charsets',
146 default=list,
146 default=list,
147 )
147 )
148 coreconfigitem('email', 'method',
148 coreconfigitem('email', 'method',
149 default='smtp',
149 default='smtp',
150 )
150 )
151 coreconfigitem('experimental', 'bundle-phases',
151 coreconfigitem('experimental', 'bundle-phases',
152 default=False,
152 default=False,
153 )
153 )
154 coreconfigitem('experimental', 'bundle2-advertise',
154 coreconfigitem('experimental', 'bundle2-advertise',
155 default=True,
155 default=True,
156 )
156 )
157 coreconfigitem('experimental', 'bundle2-output-capture',
157 coreconfigitem('experimental', 'bundle2-output-capture',
158 default=False,
158 default=False,
159 )
159 )
160 coreconfigitem('experimental', 'bundle2.pushback',
160 coreconfigitem('experimental', 'bundle2.pushback',
161 default=False,
161 default=False,
162 )
162 )
163 coreconfigitem('experimental', 'bundle2lazylocking',
163 coreconfigitem('experimental', 'bundle2lazylocking',
164 default=False,
164 default=False,
165 )
165 )
166 coreconfigitem('experimental', 'bundlecomplevel',
166 coreconfigitem('experimental', 'bundlecomplevel',
167 default=None,
167 default=None,
168 )
168 )
169 coreconfigitem('experimental', 'changegroup3',
169 coreconfigitem('experimental', 'changegroup3',
170 default=False,
170 default=False,
171 )
171 )
172 coreconfigitem('experimental', 'clientcompressionengines',
172 coreconfigitem('experimental', 'clientcompressionengines',
173 default=list,
173 default=list,
174 )
174 )
175 coreconfigitem('experimental', 'copytrace',
175 coreconfigitem('experimental', 'copytrace',
176 default='on',
176 default='on',
177 )
177 )
178 coreconfigitem('experimental', 'crecordtest',
178 coreconfigitem('experimental', 'crecordtest',
179 default=None,
179 default=None,
180 )
180 )
181 coreconfigitem('experimental', 'editortmpinhg',
181 coreconfigitem('experimental', 'editortmpinhg',
182 default=False,
182 default=False,
183 )
183 )
184 coreconfigitem('experimental', 'stabilization',
184 coreconfigitem('experimental', 'stabilization',
185 default=list,
185 default=list,
186 alias=[('experimental', 'evolution')],
186 alias=[('experimental', 'evolution')],
187 )
187 )
188 coreconfigitem('experimental', 'stabilization.bundle-obsmarker',
188 coreconfigitem('experimental', 'stabilization.bundle-obsmarker',
189 default=False,
189 default=False,
190 alias=[('experimental', 'evolution.bundle-obsmarker')],
190 alias=[('experimental', 'evolution.bundle-obsmarker')],
191 )
191 )
192 coreconfigitem('experimental', 'stabilization.track-operation',
192 coreconfigitem('experimental', 'stabilization.track-operation',
193 default=False,
193 default=False,
194 alias=[('experimental', 'evolution.track-operation')]
194 alias=[('experimental', 'evolution.track-operation')]
195 )
195 )
196 coreconfigitem('experimental', 'exportableenviron',
196 coreconfigitem('experimental', 'exportableenviron',
197 default=list,
197 default=list,
198 )
198 )
199 coreconfigitem('experimental', 'extendedheader.index',
199 coreconfigitem('experimental', 'extendedheader.index',
200 default=None,
200 default=None,
201 )
201 )
202 coreconfigitem('experimental', 'extendedheader.similarity',
202 coreconfigitem('experimental', 'extendedheader.similarity',
203 default=False,
203 default=False,
204 )
204 )
205 coreconfigitem('experimental', 'format.compression',
205 coreconfigitem('experimental', 'format.compression',
206 default='zlib',
206 default='zlib',
207 )
207 )
208 coreconfigitem('experimental', 'graphshorten',
208 coreconfigitem('experimental', 'graphshorten',
209 default=False,
209 default=False,
210 )
210 )
211 coreconfigitem('experimental', 'hook-track-tags',
211 coreconfigitem('experimental', 'hook-track-tags',
212 default=False,
212 default=False,
213 )
213 )
214 coreconfigitem('experimental', 'httppostargs',
214 coreconfigitem('experimental', 'httppostargs',
215 default=False,
215 default=False,
216 )
216 )
217 coreconfigitem('experimental', 'manifestv2',
217 coreconfigitem('experimental', 'manifestv2',
218 default=False,
218 default=False,
219 )
219 )
220 coreconfigitem('experimental', 'mergedriver',
220 coreconfigitem('experimental', 'mergedriver',
221 default=None,
221 default=None,
222 )
222 )
223 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
223 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
224 default=False,
224 default=False,
225 )
225 )
226 coreconfigitem('experimental', 'rebase.multidest',
226 coreconfigitem('experimental', 'rebase.multidest',
227 default=False,
227 default=False,
228 )
228 )
229 coreconfigitem('experimental', 'revertalternateinteractivemode',
229 coreconfigitem('experimental', 'revertalternateinteractivemode',
230 default=True,
230 default=True,
231 )
231 )
232 coreconfigitem('experimental', 'revlogv2',
232 coreconfigitem('experimental', 'revlogv2',
233 default=None,
233 default=None,
234 )
234 )
235 coreconfigitem('experimental', 'spacemovesdown',
235 coreconfigitem('experimental', 'spacemovesdown',
236 default=False,
236 default=False,
237 )
237 )
238 coreconfigitem('experimental', 'treemanifest',
238 coreconfigitem('experimental', 'treemanifest',
239 default=False,
239 default=False,
240 )
240 )
241 coreconfigitem('experimental', 'updatecheck',
241 coreconfigitem('experimental', 'updatecheck',
242 default=None,
242 default=None,
243 )
243 )
244 coreconfigitem('format', 'aggressivemergedeltas',
244 coreconfigitem('format', 'aggressivemergedeltas',
245 default=False,
245 default=False,
246 )
246 )
247 coreconfigitem('format', 'chunkcachesize',
247 coreconfigitem('format', 'chunkcachesize',
248 default=None,
248 default=None,
249 )
249 )
250 coreconfigitem('format', 'dotencode',
250 coreconfigitem('format', 'dotencode',
251 default=True,
251 default=True,
252 )
252 )
253 coreconfigitem('format', 'generaldelta',
253 coreconfigitem('format', 'generaldelta',
254 default=False,
254 default=False,
255 )
255 )
256 coreconfigitem('format', 'manifestcachesize',
256 coreconfigitem('format', 'manifestcachesize',
257 default=None,
257 default=None,
258 )
258 )
259 coreconfigitem('format', 'maxchainlen',
259 coreconfigitem('format', 'maxchainlen',
260 default=None,
260 default=None,
261 )
261 )
262 coreconfigitem('format', 'obsstore-version',
262 coreconfigitem('format', 'obsstore-version',
263 default=None,
263 default=None,
264 )
264 )
265 coreconfigitem('format', 'usefncache',
265 coreconfigitem('format', 'usefncache',
266 default=True,
266 default=True,
267 )
267 )
268 coreconfigitem('format', 'usegeneraldelta',
268 coreconfigitem('format', 'usegeneraldelta',
269 default=True,
269 default=True,
270 )
270 )
271 coreconfigitem('format', 'usestore',
271 coreconfigitem('format', 'usestore',
272 default=True,
272 default=True,
273 )
273 )
274 coreconfigitem('hostsecurity', 'ciphers',
274 coreconfigitem('hostsecurity', 'ciphers',
275 default=None,
275 default=None,
276 )
276 )
277 coreconfigitem('hostsecurity', 'disabletls10warning',
277 coreconfigitem('hostsecurity', 'disabletls10warning',
278 default=False,
278 default=False,
279 )
279 )
280 coreconfigitem('http_proxy', 'always',
280 coreconfigitem('http_proxy', 'always',
281 default=False,
281 default=False,
282 )
282 )
283 coreconfigitem('http_proxy', 'host',
283 coreconfigitem('http_proxy', 'host',
284 default=None,
284 default=None,
285 )
285 )
286 coreconfigitem('http_proxy', 'no',
286 coreconfigitem('http_proxy', 'no',
287 default=list,
287 default=list,
288 )
288 )
289 coreconfigitem('http_proxy', 'passwd',
289 coreconfigitem('http_proxy', 'passwd',
290 default=None,
290 default=None,
291 )
291 )
292 coreconfigitem('http_proxy', 'user',
292 coreconfigitem('http_proxy', 'user',
293 default=None,
293 default=None,
294 )
294 )
295 coreconfigitem('merge', 'followcopies',
295 coreconfigitem('merge', 'followcopies',
296 default=True,
296 default=True,
297 )
297 )
298 coreconfigitem('pager', 'ignore',
298 coreconfigitem('pager', 'ignore',
299 default=list,
299 default=list,
300 )
300 )
301 coreconfigitem('patch', 'eol',
301 coreconfigitem('patch', 'eol',
302 default='strict',
302 default='strict',
303 )
303 )
304 coreconfigitem('patch', 'fuzz',
304 coreconfigitem('patch', 'fuzz',
305 default=2,
305 default=2,
306 )
306 )
307 coreconfigitem('paths', 'default',
307 coreconfigitem('paths', 'default',
308 default=None,
308 default=None,
309 )
309 )
310 coreconfigitem('paths', 'default-push',
310 coreconfigitem('paths', 'default-push',
311 default=None,
311 default=None,
312 )
312 )
313 coreconfigitem('phases', 'checksubrepos',
313 coreconfigitem('phases', 'checksubrepos',
314 default='follow',
314 default='follow',
315 )
315 )
316 coreconfigitem('phases', 'publish',
316 coreconfigitem('phases', 'publish',
317 default=True,
317 default=True,
318 )
318 )
319 coreconfigitem('profiling', 'enabled',
319 coreconfigitem('profiling', 'enabled',
320 default=False,
320 default=False,
321 )
321 )
322 coreconfigitem('profiling', 'format',
322 coreconfigitem('profiling', 'format',
323 default='text',
323 default='text',
324 )
324 )
325 coreconfigitem('profiling', 'freq',
325 coreconfigitem('profiling', 'freq',
326 default=1000,
326 default=1000,
327 )
327 )
328 coreconfigitem('profiling', 'limit',
328 coreconfigitem('profiling', 'limit',
329 default=30,
329 default=30,
330 )
330 )
331 coreconfigitem('profiling', 'nested',
331 coreconfigitem('profiling', 'nested',
332 default=0,
332 default=0,
333 )
333 )
334 coreconfigitem('profiling', 'sort',
334 coreconfigitem('profiling', 'sort',
335 default='inlinetime',
335 default='inlinetime',
336 )
336 )
337 coreconfigitem('profiling', 'statformat',
337 coreconfigitem('profiling', 'statformat',
338 default='hotpath',
338 default='hotpath',
339 )
339 )
340 coreconfigitem('progress', 'assume-tty',
340 coreconfigitem('progress', 'assume-tty',
341 default=False,
341 default=False,
342 )
342 )
343 coreconfigitem('progress', 'changedelay',
343 coreconfigitem('progress', 'changedelay',
344 default=1,
344 default=1,
345 )
345 )
346 coreconfigitem('progress', 'clear-complete',
346 coreconfigitem('progress', 'clear-complete',
347 default=True,
347 default=True,
348 )
348 )
349 coreconfigitem('progress', 'debug',
349 coreconfigitem('progress', 'debug',
350 default=False,
350 default=False,
351 )
351 )
352 coreconfigitem('progress', 'delay',
352 coreconfigitem('progress', 'delay',
353 default=3,
353 default=3,
354 )
354 )
355 coreconfigitem('progress', 'disable',
355 coreconfigitem('progress', 'disable',
356 default=False,
356 default=False,
357 )
357 )
358 coreconfigitem('progress', 'estimate',
358 coreconfigitem('progress', 'estimate',
359 default=2,
359 default=2,
360 )
360 )
361 coreconfigitem('progress', 'refresh',
361 coreconfigitem('progress', 'refresh',
362 default=0.1,
362 default=0.1,
363 )
363 )
364 coreconfigitem('progress', 'width',
364 coreconfigitem('progress', 'width',
365 default=dynamicdefault,
365 default=dynamicdefault,
366 )
366 )
367 coreconfigitem('push', 'pushvars.server',
367 coreconfigitem('push', 'pushvars.server',
368 default=False,
368 default=False,
369 )
369 )
370 coreconfigitem('server', 'bundle1',
370 coreconfigitem('server', 'bundle1',
371 default=True,
371 default=True,
372 )
372 )
373 coreconfigitem('server', 'bundle1gd',
373 coreconfigitem('server', 'bundle1gd',
374 default=None,
374 default=None,
375 )
375 )
376 coreconfigitem('server', 'compressionengines',
376 coreconfigitem('server', 'compressionengines',
377 default=list,
377 default=list,
378 )
378 )
379 coreconfigitem('server', 'concurrent-push-mode',
379 coreconfigitem('server', 'concurrent-push-mode',
380 default='strict',
380 default='strict',
381 )
381 )
382 coreconfigitem('server', 'disablefullbundle',
382 coreconfigitem('server', 'disablefullbundle',
383 default=False,
383 default=False,
384 )
384 )
385 coreconfigitem('server', 'maxhttpheaderlen',
385 coreconfigitem('server', 'maxhttpheaderlen',
386 default=1024,
386 default=1024,
387 )
387 )
388 coreconfigitem('server', 'preferuncompressed',
388 coreconfigitem('server', 'preferuncompressed',
389 default=False,
389 default=False,
390 )
390 )
391 coreconfigitem('server', 'uncompressed',
391 coreconfigitem('server', 'uncompressed',
392 default=True,
392 default=True,
393 )
393 )
394 coreconfigitem('server', 'uncompressedallowsecret',
394 coreconfigitem('server', 'uncompressedallowsecret',
395 default=False,
395 default=False,
396 )
396 )
397 coreconfigitem('server', 'validate',
397 coreconfigitem('server', 'validate',
398 default=False,
398 default=False,
399 )
399 )
400 coreconfigitem('server', 'zliblevel',
400 coreconfigitem('server', 'zliblevel',
401 default=-1,
401 default=-1,
402 )
402 )
403 coreconfigitem('smtp', 'host',
403 coreconfigitem('smtp', 'host',
404 default=None,
404 default=None,
405 )
405 )
406 coreconfigitem('smtp', 'local_hostname',
406 coreconfigitem('smtp', 'local_hostname',
407 default=None,
407 default=None,
408 )
408 )
409 coreconfigitem('smtp', 'password',
409 coreconfigitem('smtp', 'password',
410 default=None,
410 default=None,
411 )
411 )
412 coreconfigitem('smtp', 'tls',
412 coreconfigitem('smtp', 'tls',
413 default='none',
413 default='none',
414 )
414 )
415 coreconfigitem('smtp', 'username',
415 coreconfigitem('smtp', 'username',
416 default=None,
416 default=None,
417 )
417 )
418 coreconfigitem('sparse', 'missingwarning',
418 coreconfigitem('sparse', 'missingwarning',
419 default=True,
419 default=True,
420 )
420 )
421 coreconfigitem('trusted', 'groups',
421 coreconfigitem('trusted', 'groups',
422 default=list,
422 default=list,
423 )
423 )
424 coreconfigitem('trusted', 'users',
424 coreconfigitem('trusted', 'users',
425 default=list,
425 default=list,
426 )
426 )
427 coreconfigitem('ui', '_usedassubrepo',
427 coreconfigitem('ui', '_usedassubrepo',
428 default=False,
428 default=False,
429 )
429 )
430 coreconfigitem('ui', 'allowemptycommit',
430 coreconfigitem('ui', 'allowemptycommit',
431 default=False,
431 default=False,
432 )
432 )
433 coreconfigitem('ui', 'archivemeta',
433 coreconfigitem('ui', 'archivemeta',
434 default=True,
434 default=True,
435 )
435 )
436 coreconfigitem('ui', 'askusername',
436 coreconfigitem('ui', 'askusername',
437 default=False,
437 default=False,
438 )
438 )
439 coreconfigitem('ui', 'clonebundlefallback',
439 coreconfigitem('ui', 'clonebundlefallback',
440 default=False,
440 default=False,
441 )
441 )
442 coreconfigitem('ui', 'clonebundleprefers',
442 coreconfigitem('ui', 'clonebundleprefers',
443 default=list,
443 default=list,
444 )
444 )
445 coreconfigitem('ui', 'clonebundles',
445 coreconfigitem('ui', 'clonebundles',
446 default=True,
446 default=True,
447 )
447 )
448 coreconfigitem('ui', 'color',
448 coreconfigitem('ui', 'color',
449 default='auto',
449 default='auto',
450 )
450 )
451 coreconfigitem('ui', 'commitsubrepos',
451 coreconfigitem('ui', 'commitsubrepos',
452 default=False,
452 default=False,
453 )
453 )
454 coreconfigitem('ui', 'debug',
454 coreconfigitem('ui', 'debug',
455 default=False,
455 default=False,
456 )
456 )
457 coreconfigitem('ui', 'debugger',
457 coreconfigitem('ui', 'debugger',
458 default=None,
458 default=None,
459 )
459 )
460 coreconfigitem('ui', 'fallbackencoding',
460 coreconfigitem('ui', 'fallbackencoding',
461 default=None,
461 default=None,
462 )
462 )
463 coreconfigitem('ui', 'forcecwd',
463 coreconfigitem('ui', 'forcecwd',
464 default=None,
464 default=None,
465 )
465 )
466 coreconfigitem('ui', 'forcemerge',
466 coreconfigitem('ui', 'forcemerge',
467 default=None,
467 default=None,
468 )
468 )
469 coreconfigitem('ui', 'formatdebug',
469 coreconfigitem('ui', 'formatdebug',
470 default=False,
470 default=False,
471 )
471 )
472 coreconfigitem('ui', 'formatjson',
472 coreconfigitem('ui', 'formatjson',
473 default=False,
473 default=False,
474 )
474 )
475 coreconfigitem('ui', 'formatted',
475 coreconfigitem('ui', 'formatted',
476 default=None,
476 default=None,
477 )
477 )
478 coreconfigitem('ui', 'graphnodetemplate',
478 coreconfigitem('ui', 'graphnodetemplate',
479 default=None,
479 default=None,
480 )
480 )
481 coreconfigitem('ui', 'http2debuglevel',
481 coreconfigitem('ui', 'http2debuglevel',
482 default=None,
482 default=None,
483 )
483 )
484 coreconfigitem('ui', 'interactive',
484 coreconfigitem('ui', 'interactive',
485 default=None,
485 default=None,
486 )
486 )
487 coreconfigitem('ui', 'interface',
487 coreconfigitem('ui', 'interface',
488 default=None,
488 default=None,
489 )
489 )
490 coreconfigitem('ui', 'logblockedtimes',
490 coreconfigitem('ui', 'logblockedtimes',
491 default=False,
491 default=False,
492 )
492 )
493 coreconfigitem('ui', 'logtemplate',
493 coreconfigitem('ui', 'logtemplate',
494 default=None,
494 default=None,
495 )
495 )
496 coreconfigitem('ui', 'merge',
496 coreconfigitem('ui', 'merge',
497 default=None,
497 default=None,
498 )
498 )
499 coreconfigitem('ui', 'mergemarkers',
499 coreconfigitem('ui', 'mergemarkers',
500 default='basic',
500 default='basic',
501 )
501 )
502 coreconfigitem('ui', 'mergemarkertemplate',
502 coreconfigitem('ui', 'mergemarkertemplate',
503 default=('{node|short} '
503 default=('{node|short} '
504 '{ifeq(tags, "tip", "", '
504 '{ifeq(tags, "tip", "", '
505 'ifeq(tags, "", "", "{tags} "))}'
505 'ifeq(tags, "", "", "{tags} "))}'
506 '{if(bookmarks, "{bookmarks} ")}'
506 '{if(bookmarks, "{bookmarks} ")}'
507 '{ifeq(branch, "default", "", "{branch} ")}'
507 '{ifeq(branch, "default", "", "{branch} ")}'
508 '- {author|user}: {desc|firstline}')
508 '- {author|user}: {desc|firstline}')
509 )
509 )
510 coreconfigitem('ui', 'nontty',
510 coreconfigitem('ui', 'nontty',
511 default=False,
511 default=False,
512 )
512 )
513 coreconfigitem('ui', 'origbackuppath',
513 coreconfigitem('ui', 'origbackuppath',
514 default=None,
514 default=None,
515 )
515 )
516 coreconfigitem('ui', 'paginate',
516 coreconfigitem('ui', 'paginate',
517 default=True,
517 default=True,
518 )
518 )
519 coreconfigitem('ui', 'patch',
519 coreconfigitem('ui', 'patch',
520 default=None,
520 default=None,
521 )
521 )
522 coreconfigitem('ui', 'portablefilenames',
522 coreconfigitem('ui', 'portablefilenames',
523 default='warn',
523 default='warn',
524 )
524 )
525 coreconfigitem('ui', 'promptecho',
525 coreconfigitem('ui', 'promptecho',
526 default=False,
526 default=False,
527 )
527 )
528 coreconfigitem('ui', 'quiet',
528 coreconfigitem('ui', 'quiet',
529 default=False,
529 default=False,
530 )
530 )
531 coreconfigitem('ui', 'quietbookmarkmove',
531 coreconfigitem('ui', 'quietbookmarkmove',
532 default=False,
532 default=False,
533 )
533 )
534 coreconfigitem('ui', 'remotecmd',
534 coreconfigitem('ui', 'remotecmd',
535 default='hg',
535 default='hg',
536 )
536 )
537 coreconfigitem('ui', 'report_untrusted',
537 coreconfigitem('ui', 'report_untrusted',
538 default=True,
538 default=True,
539 )
539 )
540 coreconfigitem('ui', 'rollback',
540 coreconfigitem('ui', 'rollback',
541 default=True,
541 default=True,
542 )
542 )
543 coreconfigitem('ui', 'slash',
543 coreconfigitem('ui', 'slash',
544 default=False,
544 default=False,
545 )
545 )
546 coreconfigitem('ui', 'ssh',
546 coreconfigitem('ui', 'ssh',
547 default='ssh',
547 default='ssh',
548 )
548 )
549 coreconfigitem('ui', 'statuscopies',
549 coreconfigitem('ui', 'statuscopies',
550 default=False,
550 default=False,
551 )
551 )
552 coreconfigitem('ui', 'strict',
552 coreconfigitem('ui', 'strict',
553 default=False,
553 default=False,
554 )
554 )
555 coreconfigitem('ui', 'style',
555 coreconfigitem('ui', 'style',
556 default='',
556 default='',
557 )
557 )
558 coreconfigitem('ui', 'supportcontact',
558 coreconfigitem('ui', 'supportcontact',
559 default=None,
559 default=None,
560 )
560 )
561 coreconfigitem('ui', 'textwidth',
561 coreconfigitem('ui', 'textwidth',
562 default=78,
562 default=78,
563 )
563 )
564 coreconfigitem('ui', 'timeout',
564 coreconfigitem('ui', 'timeout',
565 default='600',
565 default='600',
566 )
566 )
567 coreconfigitem('ui', 'traceback',
567 coreconfigitem('ui', 'traceback',
568 default=False,
568 default=False,
569 )
569 )
570 coreconfigitem('ui', 'tweakdefaults',
570 coreconfigitem('ui', 'tweakdefaults',
571 default=False,
571 default=False,
572 )
572 )
573 coreconfigitem('ui', 'usehttp2',
573 coreconfigitem('ui', 'usehttp2',
574 default=False,
574 default=False,
575 )
575 )
576 coreconfigitem('ui', 'username',
576 coreconfigitem('ui', 'username',
577 alias=[('ui', 'user')]
577 alias=[('ui', 'user')]
578 )
578 )
579 coreconfigitem('ui', 'verbose',
579 coreconfigitem('ui', 'verbose',
580 default=False,
580 default=False,
581 )
581 )
582 coreconfigitem('verify', 'skipflags',
582 coreconfigitem('verify', 'skipflags',
583 default=None,
583 default=None,
584 )
584 )
585 coreconfigitem('web', 'accesslog',
585 coreconfigitem('web', 'accesslog',
586 default='-',
586 default='-',
587 )
587 )
588 coreconfigitem('web', 'address',
588 coreconfigitem('web', 'address',
589 default='',
589 default='',
590 )
590 )
591 coreconfigitem('web', 'allow_archive',
591 coreconfigitem('web', 'allow_archive',
592 default=list,
592 default=list,
593 )
593 )
594 coreconfigitem('web', 'allow_read',
594 coreconfigitem('web', 'allow_read',
595 default=list,
595 default=list,
596 )
596 )
597 coreconfigitem('web', 'baseurl',
597 coreconfigitem('web', 'baseurl',
598 default=None,
598 default=None,
599 )
599 )
600 coreconfigitem('web', 'cacerts',
600 coreconfigitem('web', 'cacerts',
601 default=None,
601 default=None,
602 )
602 )
603 coreconfigitem('web', 'certificate',
603 coreconfigitem('web', 'certificate',
604 default=None,
604 default=None,
605 )
605 )
606 coreconfigitem('web', 'collapse',
606 coreconfigitem('web', 'collapse',
607 default=False,
607 default=False,
608 )
608 )
609 coreconfigitem('web', 'csp',
609 coreconfigitem('web', 'csp',
610 default=None,
610 default=None,
611 )
611 )
612 coreconfigitem('web', 'deny_read',
612 coreconfigitem('web', 'deny_read',
613 default=list,
613 default=list,
614 )
614 )
615 coreconfigitem('web', 'descend',
615 coreconfigitem('web', 'descend',
616 default=True,
616 default=True,
617 )
617 )
618 coreconfigitem('web', 'description',
619 default="",
620 )
618 coreconfigitem('worker', 'backgroundclose',
621 coreconfigitem('worker', 'backgroundclose',
619 default=dynamicdefault,
622 default=dynamicdefault,
620 )
623 )
621 # Windows defaults to a limit of 512 open files. A buffer of 128
624 # Windows defaults to a limit of 512 open files. A buffer of 128
622 # should give us enough headway.
625 # should give us enough headway.
623 coreconfigitem('worker', 'backgroundclosemaxqueue',
626 coreconfigitem('worker', 'backgroundclosemaxqueue',
624 default=384,
627 default=384,
625 )
628 )
626 coreconfigitem('worker', 'backgroundcloseminfilecount',
629 coreconfigitem('worker', 'backgroundcloseminfilecount',
627 default=2048,
630 default=2048,
628 )
631 )
629 coreconfigitem('worker', 'backgroundclosethreadcount',
632 coreconfigitem('worker', 'backgroundclosethreadcount',
630 default=4,
633 default=4,
631 )
634 )
632 coreconfigitem('worker', 'numcpus',
635 coreconfigitem('worker', 'numcpus',
633 default=None,
636 default=None,
634 )
637 )
@@ -1,540 +1,540 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 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import os
11 import os
12 import re
12 import re
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_NOT_FOUND,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
21 HTTP_SERVER_ERROR,
22 cspvalues,
22 cspvalues,
23 get_contact,
23 get_contact,
24 get_mtime,
24 get_mtime,
25 ismember,
25 ismember,
26 paritygen,
26 paritygen,
27 staticfile,
27 staticfile,
28 )
28 )
29 from .request import wsgirequest
29 from .request import wsgirequest
30
30
31 from .. import (
31 from .. import (
32 encoding,
32 encoding,
33 error,
33 error,
34 hg,
34 hg,
35 profiling,
35 profiling,
36 scmutil,
36 scmutil,
37 templater,
37 templater,
38 ui as uimod,
38 ui as uimod,
39 util,
39 util,
40 )
40 )
41
41
42 from . import (
42 from . import (
43 hgweb_mod,
43 hgweb_mod,
44 webutil,
44 webutil,
45 wsgicgi,
45 wsgicgi,
46 )
46 )
47
47
48 def cleannames(items):
48 def cleannames(items):
49 return [(util.pconvert(name).strip('/'), path) for name, path in items]
49 return [(util.pconvert(name).strip('/'), path) for name, path in items]
50
50
51 def findrepos(paths):
51 def findrepos(paths):
52 repos = []
52 repos = []
53 for prefix, root in cleannames(paths):
53 for prefix, root in cleannames(paths):
54 roothead, roottail = os.path.split(root)
54 roothead, roottail = os.path.split(root)
55 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
55 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
56 # /bar/ be served as as foo/N .
56 # /bar/ be served as as foo/N .
57 # '*' will not search inside dirs with .hg (except .hg/patches),
57 # '*' will not search inside dirs with .hg (except .hg/patches),
58 # '**' will search inside dirs with .hg (and thus also find subrepos).
58 # '**' will search inside dirs with .hg (and thus also find subrepos).
59 try:
59 try:
60 recurse = {'*': False, '**': True}[roottail]
60 recurse = {'*': False, '**': True}[roottail]
61 except KeyError:
61 except KeyError:
62 repos.append((prefix, root))
62 repos.append((prefix, root))
63 continue
63 continue
64 roothead = os.path.normpath(os.path.abspath(roothead))
64 roothead = os.path.normpath(os.path.abspath(roothead))
65 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
65 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
66 repos.extend(urlrepos(prefix, roothead, paths))
66 repos.extend(urlrepos(prefix, roothead, paths))
67 return repos
67 return repos
68
68
69 def urlrepos(prefix, roothead, paths):
69 def urlrepos(prefix, roothead, paths):
70 """yield url paths and filesystem paths from a list of repo paths
70 """yield url paths and filesystem paths from a list of repo paths
71
71
72 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
72 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
73 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
73 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
74 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
74 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
75 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
75 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
76 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
76 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
77 """
77 """
78 for path in paths:
78 for path in paths:
79 path = os.path.normpath(path)
79 path = os.path.normpath(path)
80 yield (prefix + '/' +
80 yield (prefix + '/' +
81 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
81 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
82
82
83 def geturlcgivars(baseurl, port):
83 def geturlcgivars(baseurl, port):
84 """
84 """
85 Extract CGI variables from baseurl
85 Extract CGI variables from baseurl
86
86
87 >>> geturlcgivars(b"http://host.org/base", b"80")
87 >>> geturlcgivars(b"http://host.org/base", b"80")
88 ('host.org', '80', '/base')
88 ('host.org', '80', '/base')
89 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
89 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
90 ('host.org', '8000', '/base')
90 ('host.org', '8000', '/base')
91 >>> geturlcgivars(b'/base', 8000)
91 >>> geturlcgivars(b'/base', 8000)
92 ('', '8000', '/base')
92 ('', '8000', '/base')
93 >>> geturlcgivars(b"base", b'8000')
93 >>> geturlcgivars(b"base", b'8000')
94 ('', '8000', '/base')
94 ('', '8000', '/base')
95 >>> geturlcgivars(b"http://host", b'8000')
95 >>> geturlcgivars(b"http://host", b'8000')
96 ('host', '8000', '/')
96 ('host', '8000', '/')
97 >>> geturlcgivars(b"http://host/", b'8000')
97 >>> geturlcgivars(b"http://host/", b'8000')
98 ('host', '8000', '/')
98 ('host', '8000', '/')
99 """
99 """
100 u = util.url(baseurl)
100 u = util.url(baseurl)
101 name = u.host or ''
101 name = u.host or ''
102 if u.port:
102 if u.port:
103 port = u.port
103 port = u.port
104 path = u.path or ""
104 path = u.path or ""
105 if not path.startswith('/'):
105 if not path.startswith('/'):
106 path = '/' + path
106 path = '/' + path
107
107
108 return name, str(port), path
108 return name, str(port), path
109
109
110 class hgwebdir(object):
110 class hgwebdir(object):
111 """HTTP server for multiple repositories.
111 """HTTP server for multiple repositories.
112
112
113 Given a configuration, different repositories will be served depending
113 Given a configuration, different repositories will be served depending
114 on the request path.
114 on the request path.
115
115
116 Instances are typically used as WSGI applications.
116 Instances are typically used as WSGI applications.
117 """
117 """
118 def __init__(self, conf, baseui=None):
118 def __init__(self, conf, baseui=None):
119 self.conf = conf
119 self.conf = conf
120 self.baseui = baseui
120 self.baseui = baseui
121 self.ui = None
121 self.ui = None
122 self.lastrefresh = 0
122 self.lastrefresh = 0
123 self.motd = None
123 self.motd = None
124 self.refresh()
124 self.refresh()
125
125
126 def refresh(self):
126 def refresh(self):
127 refreshinterval = 20
127 refreshinterval = 20
128 if self.ui:
128 if self.ui:
129 refreshinterval = self.ui.configint('web', 'refreshinterval',
129 refreshinterval = self.ui.configint('web', 'refreshinterval',
130 refreshinterval)
130 refreshinterval)
131
131
132 # refreshinterval <= 0 means to always refresh.
132 # refreshinterval <= 0 means to always refresh.
133 if (refreshinterval > 0 and
133 if (refreshinterval > 0 and
134 self.lastrefresh + refreshinterval > time.time()):
134 self.lastrefresh + refreshinterval > time.time()):
135 return
135 return
136
136
137 if self.baseui:
137 if self.baseui:
138 u = self.baseui.copy()
138 u = self.baseui.copy()
139 else:
139 else:
140 u = uimod.ui.load()
140 u = uimod.ui.load()
141 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
141 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
142 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
142 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
143 # displaying bundling progress bar while serving feels wrong and may
143 # displaying bundling progress bar while serving feels wrong and may
144 # break some wsgi implementations.
144 # break some wsgi implementations.
145 u.setconfig('progress', 'disable', 'true', 'hgweb')
145 u.setconfig('progress', 'disable', 'true', 'hgweb')
146
146
147 if not isinstance(self.conf, (dict, list, tuple)):
147 if not isinstance(self.conf, (dict, list, tuple)):
148 map = {'paths': 'hgweb-paths'}
148 map = {'paths': 'hgweb-paths'}
149 if not os.path.exists(self.conf):
149 if not os.path.exists(self.conf):
150 raise error.Abort(_('config file %s not found!') % self.conf)
150 raise error.Abort(_('config file %s not found!') % self.conf)
151 u.readconfig(self.conf, remap=map, trust=True)
151 u.readconfig(self.conf, remap=map, trust=True)
152 paths = []
152 paths = []
153 for name, ignored in u.configitems('hgweb-paths'):
153 for name, ignored in u.configitems('hgweb-paths'):
154 for path in u.configlist('hgweb-paths', name):
154 for path in u.configlist('hgweb-paths', name):
155 paths.append((name, path))
155 paths.append((name, path))
156 elif isinstance(self.conf, (list, tuple)):
156 elif isinstance(self.conf, (list, tuple)):
157 paths = self.conf
157 paths = self.conf
158 elif isinstance(self.conf, dict):
158 elif isinstance(self.conf, dict):
159 paths = self.conf.items()
159 paths = self.conf.items()
160
160
161 repos = findrepos(paths)
161 repos = findrepos(paths)
162 for prefix, root in u.configitems('collections'):
162 for prefix, root in u.configitems('collections'):
163 prefix = util.pconvert(prefix)
163 prefix = util.pconvert(prefix)
164 for path in scmutil.walkrepos(root, followsym=True):
164 for path in scmutil.walkrepos(root, followsym=True):
165 repo = os.path.normpath(path)
165 repo = os.path.normpath(path)
166 name = util.pconvert(repo)
166 name = util.pconvert(repo)
167 if name.startswith(prefix):
167 if name.startswith(prefix):
168 name = name[len(prefix):]
168 name = name[len(prefix):]
169 repos.append((name.lstrip('/'), repo))
169 repos.append((name.lstrip('/'), repo))
170
170
171 self.repos = repos
171 self.repos = repos
172 self.ui = u
172 self.ui = u
173 encoding.encoding = self.ui.config('web', 'encoding',
173 encoding.encoding = self.ui.config('web', 'encoding',
174 encoding.encoding)
174 encoding.encoding)
175 self.style = self.ui.config('web', 'style', 'paper')
175 self.style = self.ui.config('web', 'style', 'paper')
176 self.templatepath = self.ui.config('web', 'templates', None)
176 self.templatepath = self.ui.config('web', 'templates', None)
177 self.stripecount = self.ui.config('web', 'stripes', 1)
177 self.stripecount = self.ui.config('web', 'stripes', 1)
178 if self.stripecount:
178 if self.stripecount:
179 self.stripecount = int(self.stripecount)
179 self.stripecount = int(self.stripecount)
180 self._baseurl = self.ui.config('web', 'baseurl')
180 self._baseurl = self.ui.config('web', 'baseurl')
181 prefix = self.ui.config('web', 'prefix', '')
181 prefix = self.ui.config('web', 'prefix', '')
182 if prefix.startswith('/'):
182 if prefix.startswith('/'):
183 prefix = prefix[1:]
183 prefix = prefix[1:]
184 if prefix.endswith('/'):
184 if prefix.endswith('/'):
185 prefix = prefix[:-1]
185 prefix = prefix[:-1]
186 self.prefix = prefix
186 self.prefix = prefix
187 self.lastrefresh = time.time()
187 self.lastrefresh = time.time()
188
188
189 def run(self):
189 def run(self):
190 if not encoding.environ.get('GATEWAY_INTERFACE',
190 if not encoding.environ.get('GATEWAY_INTERFACE',
191 '').startswith("CGI/1."):
191 '').startswith("CGI/1."):
192 raise RuntimeError("This function is only intended to be "
192 raise RuntimeError("This function is only intended to be "
193 "called while running as a CGI script.")
193 "called while running as a CGI script.")
194 wsgicgi.launch(self)
194 wsgicgi.launch(self)
195
195
196 def __call__(self, env, respond):
196 def __call__(self, env, respond):
197 req = wsgirequest(env, respond)
197 req = wsgirequest(env, respond)
198 return self.run_wsgi(req)
198 return self.run_wsgi(req)
199
199
200 def read_allowed(self, ui, req):
200 def read_allowed(self, ui, req):
201 """Check allow_read and deny_read config options of a repo's ui object
201 """Check allow_read and deny_read config options of a repo's ui object
202 to determine user permissions. By default, with neither option set (or
202 to determine user permissions. By default, with neither option set (or
203 both empty), allow all users to read the repo. There are two ways a
203 both empty), allow all users to read the repo. There are two ways a
204 user can be denied read access: (1) deny_read is not empty, and the
204 user can be denied read access: (1) deny_read is not empty, and the
205 user is unauthenticated or deny_read contains user (or *), and (2)
205 user is unauthenticated or deny_read contains user (or *), and (2)
206 allow_read is not empty and the user is not in allow_read. Return True
206 allow_read is not empty and the user is not in allow_read. Return True
207 if user is allowed to read the repo, else return False."""
207 if user is allowed to read the repo, else return False."""
208
208
209 user = req.env.get('REMOTE_USER')
209 user = req.env.get('REMOTE_USER')
210
210
211 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
211 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
212 if deny_read and (not user or ismember(ui, user, deny_read)):
212 if deny_read and (not user or ismember(ui, user, deny_read)):
213 return False
213 return False
214
214
215 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
215 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
216 # by default, allow reading if no allow_read option has been set
216 # by default, allow reading if no allow_read option has been set
217 if (not allow_read) or ismember(ui, user, allow_read):
217 if (not allow_read) or ismember(ui, user, allow_read):
218 return True
218 return True
219
219
220 return False
220 return False
221
221
222 def run_wsgi(self, req):
222 def run_wsgi(self, req):
223 profile = self.ui.configbool('profiling', 'enabled')
223 profile = self.ui.configbool('profiling', 'enabled')
224 with profiling.profile(self.ui, enabled=profile):
224 with profiling.profile(self.ui, enabled=profile):
225 for r in self._runwsgi(req):
225 for r in self._runwsgi(req):
226 yield r
226 yield r
227
227
228 def _runwsgi(self, req):
228 def _runwsgi(self, req):
229 try:
229 try:
230 self.refresh()
230 self.refresh()
231
231
232 csp, nonce = cspvalues(self.ui)
232 csp, nonce = cspvalues(self.ui)
233 if csp:
233 if csp:
234 req.headers.append(('Content-Security-Policy', csp))
234 req.headers.append(('Content-Security-Policy', csp))
235
235
236 virtual = req.env.get("PATH_INFO", "").strip('/')
236 virtual = req.env.get("PATH_INFO", "").strip('/')
237 tmpl = self.templater(req, nonce)
237 tmpl = self.templater(req, nonce)
238 ctype = tmpl('mimetype', encoding=encoding.encoding)
238 ctype = tmpl('mimetype', encoding=encoding.encoding)
239 ctype = templater.stringify(ctype)
239 ctype = templater.stringify(ctype)
240
240
241 # a static file
241 # a static file
242 if virtual.startswith('static/') or 'static' in req.form:
242 if virtual.startswith('static/') or 'static' in req.form:
243 if virtual.startswith('static/'):
243 if virtual.startswith('static/'):
244 fname = virtual[7:]
244 fname = virtual[7:]
245 else:
245 else:
246 fname = req.form['static'][0]
246 fname = req.form['static'][0]
247 static = self.ui.config("web", "static", None,
247 static = self.ui.config("web", "static", None,
248 untrusted=False)
248 untrusted=False)
249 if not static:
249 if not static:
250 tp = self.templatepath or templater.templatepaths()
250 tp = self.templatepath or templater.templatepaths()
251 if isinstance(tp, str):
251 if isinstance(tp, str):
252 tp = [tp]
252 tp = [tp]
253 static = [os.path.join(p, 'static') for p in tp]
253 static = [os.path.join(p, 'static') for p in tp]
254 staticfile(static, fname, req)
254 staticfile(static, fname, req)
255 return []
255 return []
256
256
257 # top-level index
257 # top-level index
258
258
259 repos = dict(self.repos)
259 repos = dict(self.repos)
260
260
261 if (not virtual or virtual == 'index') and virtual not in repos:
261 if (not virtual or virtual == 'index') and virtual not in repos:
262 req.respond(HTTP_OK, ctype)
262 req.respond(HTTP_OK, ctype)
263 return self.makeindex(req, tmpl)
263 return self.makeindex(req, tmpl)
264
264
265 # nested indexes and hgwebs
265 # nested indexes and hgwebs
266
266
267 if virtual.endswith('/index') and virtual not in repos:
267 if virtual.endswith('/index') and virtual not in repos:
268 subdir = virtual[:-len('index')]
268 subdir = virtual[:-len('index')]
269 if any(r.startswith(subdir) for r in repos):
269 if any(r.startswith(subdir) for r in repos):
270 req.respond(HTTP_OK, ctype)
270 req.respond(HTTP_OK, ctype)
271 return self.makeindex(req, tmpl, subdir)
271 return self.makeindex(req, tmpl, subdir)
272
272
273 def _virtualdirs():
273 def _virtualdirs():
274 # Check the full virtual path, each parent, and the root ('')
274 # Check the full virtual path, each parent, and the root ('')
275 if virtual != '':
275 if virtual != '':
276 yield virtual
276 yield virtual
277
277
278 for p in util.finddirs(virtual):
278 for p in util.finddirs(virtual):
279 yield p
279 yield p
280
280
281 yield ''
281 yield ''
282
282
283 for virtualrepo in _virtualdirs():
283 for virtualrepo in _virtualdirs():
284 real = repos.get(virtualrepo)
284 real = repos.get(virtualrepo)
285 if real:
285 if real:
286 req.env['REPO_NAME'] = virtualrepo
286 req.env['REPO_NAME'] = virtualrepo
287 try:
287 try:
288 # ensure caller gets private copy of ui
288 # ensure caller gets private copy of ui
289 repo = hg.repository(self.ui.copy(), real)
289 repo = hg.repository(self.ui.copy(), real)
290 return hgweb_mod.hgweb(repo).run_wsgi(req)
290 return hgweb_mod.hgweb(repo).run_wsgi(req)
291 except IOError as inst:
291 except IOError as inst:
292 msg = encoding.strtolocal(inst.strerror)
292 msg = encoding.strtolocal(inst.strerror)
293 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
293 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
294 except error.RepoError as inst:
294 except error.RepoError as inst:
295 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
295 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
296
296
297 # browse subdirectories
297 # browse subdirectories
298 subdir = virtual + '/'
298 subdir = virtual + '/'
299 if [r for r in repos if r.startswith(subdir)]:
299 if [r for r in repos if r.startswith(subdir)]:
300 req.respond(HTTP_OK, ctype)
300 req.respond(HTTP_OK, ctype)
301 return self.makeindex(req, tmpl, subdir)
301 return self.makeindex(req, tmpl, subdir)
302
302
303 # prefixes not found
303 # prefixes not found
304 req.respond(HTTP_NOT_FOUND, ctype)
304 req.respond(HTTP_NOT_FOUND, ctype)
305 return tmpl("notfound", repo=virtual)
305 return tmpl("notfound", repo=virtual)
306
306
307 except ErrorResponse as err:
307 except ErrorResponse as err:
308 req.respond(err, ctype)
308 req.respond(err, ctype)
309 return tmpl('error', error=err.message or '')
309 return tmpl('error', error=err.message or '')
310 finally:
310 finally:
311 tmpl = None
311 tmpl = None
312
312
313 def makeindex(self, req, tmpl, subdir=""):
313 def makeindex(self, req, tmpl, subdir=""):
314
314
315 def archivelist(ui, nodeid, url):
315 def archivelist(ui, nodeid, url):
316 allowed = ui.configlist("web", "allow_archive", untrusted=True)
316 allowed = ui.configlist("web", "allow_archive", untrusted=True)
317 archives = []
317 archives = []
318 for typ, spec in hgweb_mod.archivespecs.iteritems():
318 for typ, spec in hgweb_mod.archivespecs.iteritems():
319 if typ in allowed or ui.configbool("web", "allow" + typ,
319 if typ in allowed or ui.configbool("web", "allow" + typ,
320 untrusted=True):
320 untrusted=True):
321 archives.append({"type" : typ, "extension": spec[2],
321 archives.append({"type" : typ, "extension": spec[2],
322 "node": nodeid, "url": url})
322 "node": nodeid, "url": url})
323 return archives
323 return archives
324
324
325 def rawentries(subdir="", **map):
325 def rawentries(subdir="", **map):
326
326
327 descend = self.ui.configbool('web', 'descend')
327 descend = self.ui.configbool('web', 'descend')
328 collapse = self.ui.configbool('web', 'collapse')
328 collapse = self.ui.configbool('web', 'collapse')
329 seenrepos = set()
329 seenrepos = set()
330 seendirs = set()
330 seendirs = set()
331 for name, path in self.repos:
331 for name, path in self.repos:
332
332
333 if not name.startswith(subdir):
333 if not name.startswith(subdir):
334 continue
334 continue
335 name = name[len(subdir):]
335 name = name[len(subdir):]
336 directory = False
336 directory = False
337
337
338 if '/' in name:
338 if '/' in name:
339 if not descend:
339 if not descend:
340 continue
340 continue
341
341
342 nameparts = name.split('/')
342 nameparts = name.split('/')
343 rootname = nameparts[0]
343 rootname = nameparts[0]
344
344
345 if not collapse:
345 if not collapse:
346 pass
346 pass
347 elif rootname in seendirs:
347 elif rootname in seendirs:
348 continue
348 continue
349 elif rootname in seenrepos:
349 elif rootname in seenrepos:
350 pass
350 pass
351 else:
351 else:
352 directory = True
352 directory = True
353 name = rootname
353 name = rootname
354
354
355 # redefine the path to refer to the directory
355 # redefine the path to refer to the directory
356 discarded = '/'.join(nameparts[1:])
356 discarded = '/'.join(nameparts[1:])
357
357
358 # remove name parts plus accompanying slash
358 # remove name parts plus accompanying slash
359 path = path[:-len(discarded) - 1]
359 path = path[:-len(discarded) - 1]
360
360
361 try:
361 try:
362 r = hg.repository(self.ui, path)
362 r = hg.repository(self.ui, path)
363 directory = False
363 directory = False
364 except (IOError, error.RepoError):
364 except (IOError, error.RepoError):
365 pass
365 pass
366
366
367 parts = [name]
367 parts = [name]
368 parts.insert(0, '/' + subdir.rstrip('/'))
368 parts.insert(0, '/' + subdir.rstrip('/'))
369 if req.env['SCRIPT_NAME']:
369 if req.env['SCRIPT_NAME']:
370 parts.insert(0, req.env['SCRIPT_NAME'])
370 parts.insert(0, req.env['SCRIPT_NAME'])
371 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
371 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
372
372
373 # show either a directory entry or a repository
373 # show either a directory entry or a repository
374 if directory:
374 if directory:
375 # get the directory's time information
375 # get the directory's time information
376 try:
376 try:
377 d = (get_mtime(path), util.makedate()[1])
377 d = (get_mtime(path), util.makedate()[1])
378 except OSError:
378 except OSError:
379 continue
379 continue
380
380
381 # add '/' to the name to make it obvious that
381 # add '/' to the name to make it obvious that
382 # the entry is a directory, not a regular repository
382 # the entry is a directory, not a regular repository
383 row = {'contact': "",
383 row = {'contact': "",
384 'contact_sort': "",
384 'contact_sort': "",
385 'name': name + '/',
385 'name': name + '/',
386 'name_sort': name,
386 'name_sort': name,
387 'url': url,
387 'url': url,
388 'description': "",
388 'description': "",
389 'description_sort': "",
389 'description_sort': "",
390 'lastchange': d,
390 'lastchange': d,
391 'lastchange_sort': d[1]-d[0],
391 'lastchange_sort': d[1]-d[0],
392 'archives': [],
392 'archives': [],
393 'isdirectory': True,
393 'isdirectory': True,
394 'labels': [],
394 'labels': [],
395 }
395 }
396
396
397 seendirs.add(name)
397 seendirs.add(name)
398 yield row
398 yield row
399 continue
399 continue
400
400
401 u = self.ui.copy()
401 u = self.ui.copy()
402 try:
402 try:
403 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
403 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
404 except Exception as e:
404 except Exception as e:
405 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
405 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
406 continue
406 continue
407 def get(section, name, default=uimod._unset):
407 def get(section, name, default=uimod._unset):
408 return u.config(section, name, default, untrusted=True)
408 return u.config(section, name, default, untrusted=True)
409
409
410 if u.configbool("web", "hidden", untrusted=True):
410 if u.configbool("web", "hidden", untrusted=True):
411 continue
411 continue
412
412
413 if not self.read_allowed(u, req):
413 if not self.read_allowed(u, req):
414 continue
414 continue
415
415
416 # update time with local timezone
416 # update time with local timezone
417 try:
417 try:
418 r = hg.repository(self.ui, path)
418 r = hg.repository(self.ui, path)
419 except IOError:
419 except IOError:
420 u.warn(_('error accessing repository at %s\n') % path)
420 u.warn(_('error accessing repository at %s\n') % path)
421 continue
421 continue
422 except error.RepoError:
422 except error.RepoError:
423 u.warn(_('error accessing repository at %s\n') % path)
423 u.warn(_('error accessing repository at %s\n') % path)
424 continue
424 continue
425 try:
425 try:
426 d = (get_mtime(r.spath), util.makedate()[1])
426 d = (get_mtime(r.spath), util.makedate()[1])
427 except OSError:
427 except OSError:
428 continue
428 continue
429
429
430 contact = get_contact(get)
430 contact = get_contact(get)
431 description = get("web", "description", "")
431 description = get("web", "description")
432 seenrepos.add(name)
432 seenrepos.add(name)
433 name = get("web", "name", name)
433 name = get("web", "name", name)
434 row = {'contact': contact or "unknown",
434 row = {'contact': contact or "unknown",
435 'contact_sort': contact.upper() or "unknown",
435 'contact_sort': contact.upper() or "unknown",
436 'name': name,
436 'name': name,
437 'name_sort': name,
437 'name_sort': name,
438 'url': url,
438 'url': url,
439 'description': description or "unknown",
439 'description': description or "unknown",
440 'description_sort': description.upper() or "unknown",
440 'description_sort': description.upper() or "unknown",
441 'lastchange': d,
441 'lastchange': d,
442 'lastchange_sort': d[1]-d[0],
442 'lastchange_sort': d[1]-d[0],
443 'archives': archivelist(u, "tip", url),
443 'archives': archivelist(u, "tip", url),
444 'isdirectory': None,
444 'isdirectory': None,
445 'labels': u.configlist('web', 'labels', untrusted=True),
445 'labels': u.configlist('web', 'labels', untrusted=True),
446 }
446 }
447
447
448 yield row
448 yield row
449
449
450 sortdefault = None, False
450 sortdefault = None, False
451 def entries(sortcolumn="", descending=False, subdir="", **map):
451 def entries(sortcolumn="", descending=False, subdir="", **map):
452 rows = rawentries(subdir=subdir, **map)
452 rows = rawentries(subdir=subdir, **map)
453
453
454 if sortcolumn and sortdefault != (sortcolumn, descending):
454 if sortcolumn and sortdefault != (sortcolumn, descending):
455 sortkey = '%s_sort' % sortcolumn
455 sortkey = '%s_sort' % sortcolumn
456 rows = sorted(rows, key=lambda x: x[sortkey],
456 rows = sorted(rows, key=lambda x: x[sortkey],
457 reverse=descending)
457 reverse=descending)
458 for row, parity in zip(rows, paritygen(self.stripecount)):
458 for row, parity in zip(rows, paritygen(self.stripecount)):
459 row['parity'] = parity
459 row['parity'] = parity
460 yield row
460 yield row
461
461
462 self.refresh()
462 self.refresh()
463 sortable = ["name", "description", "contact", "lastchange"]
463 sortable = ["name", "description", "contact", "lastchange"]
464 sortcolumn, descending = sortdefault
464 sortcolumn, descending = sortdefault
465 if 'sort' in req.form:
465 if 'sort' in req.form:
466 sortcolumn = req.form['sort'][0]
466 sortcolumn = req.form['sort'][0]
467 descending = sortcolumn.startswith('-')
467 descending = sortcolumn.startswith('-')
468 if descending:
468 if descending:
469 sortcolumn = sortcolumn[1:]
469 sortcolumn = sortcolumn[1:]
470 if sortcolumn not in sortable:
470 if sortcolumn not in sortable:
471 sortcolumn = ""
471 sortcolumn = ""
472
472
473 sort = [("sort_%s" % column,
473 sort = [("sort_%s" % column,
474 "%s%s" % ((not descending and column == sortcolumn)
474 "%s%s" % ((not descending and column == sortcolumn)
475 and "-" or "", column))
475 and "-" or "", column))
476 for column in sortable]
476 for column in sortable]
477
477
478 self.refresh()
478 self.refresh()
479 self.updatereqenv(req.env)
479 self.updatereqenv(req.env)
480
480
481 return tmpl("index", entries=entries, subdir=subdir,
481 return tmpl("index", entries=entries, subdir=subdir,
482 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
482 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
483 sortcolumn=sortcolumn, descending=descending,
483 sortcolumn=sortcolumn, descending=descending,
484 **dict(sort))
484 **dict(sort))
485
485
486 def templater(self, req, nonce):
486 def templater(self, req, nonce):
487
487
488 def motd(**map):
488 def motd(**map):
489 if self.motd is not None:
489 if self.motd is not None:
490 yield self.motd
490 yield self.motd
491 else:
491 else:
492 yield config('web', 'motd', '')
492 yield config('web', 'motd', '')
493
493
494 def config(section, name, default=uimod._unset, untrusted=True):
494 def config(section, name, default=uimod._unset, untrusted=True):
495 return self.ui.config(section, name, default, untrusted)
495 return self.ui.config(section, name, default, untrusted)
496
496
497 self.updatereqenv(req.env)
497 self.updatereqenv(req.env)
498
498
499 url = req.env.get('SCRIPT_NAME', '')
499 url = req.env.get('SCRIPT_NAME', '')
500 if not url.endswith('/'):
500 if not url.endswith('/'):
501 url += '/'
501 url += '/'
502
502
503 vars = {}
503 vars = {}
504 styles = (
504 styles = (
505 req.form.get('style', [None])[0],
505 req.form.get('style', [None])[0],
506 config('web', 'style'),
506 config('web', 'style'),
507 'paper'
507 'paper'
508 )
508 )
509 style, mapfile = templater.stylemap(styles, self.templatepath)
509 style, mapfile = templater.stylemap(styles, self.templatepath)
510 if style == styles[0]:
510 if style == styles[0]:
511 vars['style'] = style
511 vars['style'] = style
512
512
513 start = url[-1] == '?' and '&' or '?'
513 start = url[-1] == '?' and '&' or '?'
514 sessionvars = webutil.sessionvars(vars, start)
514 sessionvars = webutil.sessionvars(vars, start)
515 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
515 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
516 logoimg = config('web', 'logoimg', 'hglogo.png')
516 logoimg = config('web', 'logoimg', 'hglogo.png')
517 staticurl = config('web', 'staticurl') or url + 'static/'
517 staticurl = config('web', 'staticurl') or url + 'static/'
518 if not staticurl.endswith('/'):
518 if not staticurl.endswith('/'):
519 staticurl += '/'
519 staticurl += '/'
520
520
521 defaults = {
521 defaults = {
522 "encoding": encoding.encoding,
522 "encoding": encoding.encoding,
523 "motd": motd,
523 "motd": motd,
524 "url": url,
524 "url": url,
525 "logourl": logourl,
525 "logourl": logourl,
526 "logoimg": logoimg,
526 "logoimg": logoimg,
527 "staticurl": staticurl,
527 "staticurl": staticurl,
528 "sessionvars": sessionvars,
528 "sessionvars": sessionvars,
529 "style": style,
529 "style": style,
530 "nonce": nonce,
530 "nonce": nonce,
531 }
531 }
532 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
532 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
533 return tmpl
533 return tmpl
534
534
535 def updatereqenv(self, env):
535 def updatereqenv(self, env):
536 if self._baseurl is not None:
536 if self._baseurl is not None:
537 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
537 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
538 env['SERVER_NAME'] = name
538 env['SERVER_NAME'] = name
539 env['SERVER_PORT'] = port
539 env['SERVER_PORT'] = port
540 env['SCRIPT_NAME'] = path
540 env['SCRIPT_NAME'] = path
@@ -1,1385 +1,1388 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import cgi
10 import cgi
11 import copy
11 import copy
12 import mimetypes
12 import mimetypes
13 import os
13 import os
14 import re
14 import re
15
15
16 from ..i18n import _
16 from ..i18n import _
17 from ..node import hex, short
17 from ..node import hex, short
18
18
19 from .common import (
19 from .common import (
20 ErrorResponse,
20 ErrorResponse,
21 HTTP_FORBIDDEN,
21 HTTP_FORBIDDEN,
22 HTTP_NOT_FOUND,
22 HTTP_NOT_FOUND,
23 HTTP_OK,
23 HTTP_OK,
24 get_contact,
24 get_contact,
25 paritygen,
25 paritygen,
26 staticfile,
26 staticfile,
27 )
27 )
28
28
29 from .. import (
29 from .. import (
30 archival,
30 archival,
31 dagop,
31 dagop,
32 encoding,
32 encoding,
33 error,
33 error,
34 graphmod,
34 graphmod,
35 revset,
35 revset,
36 revsetlang,
36 revsetlang,
37 scmutil,
37 scmutil,
38 smartset,
38 smartset,
39 templatefilters,
39 templatefilters,
40 templater,
40 templater,
41 util,
41 util,
42 )
42 )
43
43
44 from . import (
44 from . import (
45 webutil,
45 webutil,
46 )
46 )
47
47
48 __all__ = []
48 __all__ = []
49 commands = {}
49 commands = {}
50
50
51 class webcommand(object):
51 class webcommand(object):
52 """Decorator used to register a web command handler.
52 """Decorator used to register a web command handler.
53
53
54 The decorator takes as its positional arguments the name/path the
54 The decorator takes as its positional arguments the name/path the
55 command should be accessible under.
55 command should be accessible under.
56
56
57 Usage:
57 Usage:
58
58
59 @webcommand('mycommand')
59 @webcommand('mycommand')
60 def mycommand(web, req, tmpl):
60 def mycommand(web, req, tmpl):
61 pass
61 pass
62 """
62 """
63
63
64 def __init__(self, name):
64 def __init__(self, name):
65 self.name = name
65 self.name = name
66
66
67 def __call__(self, func):
67 def __call__(self, func):
68 __all__.append(self.name)
68 __all__.append(self.name)
69 commands[self.name] = func
69 commands[self.name] = func
70 return func
70 return func
71
71
72 @webcommand('log')
72 @webcommand('log')
73 def log(web, req, tmpl):
73 def log(web, req, tmpl):
74 """
74 """
75 /log[/{revision}[/{path}]]
75 /log[/{revision}[/{path}]]
76 --------------------------
76 --------------------------
77
77
78 Show repository or file history.
78 Show repository or file history.
79
79
80 For URLs of the form ``/log/{revision}``, a list of changesets starting at
80 For URLs of the form ``/log/{revision}``, a list of changesets starting at
81 the specified changeset identifier is shown. If ``{revision}`` is not
81 the specified changeset identifier is shown. If ``{revision}`` is not
82 defined, the default is ``tip``. This form is equivalent to the
82 defined, the default is ``tip``. This form is equivalent to the
83 ``changelog`` handler.
83 ``changelog`` handler.
84
84
85 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
85 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
86 file will be shown. This form is equivalent to the ``filelog`` handler.
86 file will be shown. This form is equivalent to the ``filelog`` handler.
87 """
87 """
88
88
89 if 'file' in req.form and req.form['file'][0]:
89 if 'file' in req.form and req.form['file'][0]:
90 return filelog(web, req, tmpl)
90 return filelog(web, req, tmpl)
91 else:
91 else:
92 return changelog(web, req, tmpl)
92 return changelog(web, req, tmpl)
93
93
94 @webcommand('rawfile')
94 @webcommand('rawfile')
95 def rawfile(web, req, tmpl):
95 def rawfile(web, req, tmpl):
96 guessmime = web.configbool('web', 'guessmime', False)
96 guessmime = web.configbool('web', 'guessmime', False)
97
97
98 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
98 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
99 if not path:
99 if not path:
100 content = manifest(web, req, tmpl)
100 content = manifest(web, req, tmpl)
101 req.respond(HTTP_OK, web.ctype)
101 req.respond(HTTP_OK, web.ctype)
102 return content
102 return content
103
103
104 try:
104 try:
105 fctx = webutil.filectx(web.repo, req)
105 fctx = webutil.filectx(web.repo, req)
106 except error.LookupError as inst:
106 except error.LookupError as inst:
107 try:
107 try:
108 content = manifest(web, req, tmpl)
108 content = manifest(web, req, tmpl)
109 req.respond(HTTP_OK, web.ctype)
109 req.respond(HTTP_OK, web.ctype)
110 return content
110 return content
111 except ErrorResponse:
111 except ErrorResponse:
112 raise inst
112 raise inst
113
113
114 path = fctx.path()
114 path = fctx.path()
115 text = fctx.data()
115 text = fctx.data()
116 mt = 'application/binary'
116 mt = 'application/binary'
117 if guessmime:
117 if guessmime:
118 mt = mimetypes.guess_type(path)[0]
118 mt = mimetypes.guess_type(path)[0]
119 if mt is None:
119 if mt is None:
120 if util.binary(text):
120 if util.binary(text):
121 mt = 'application/binary'
121 mt = 'application/binary'
122 else:
122 else:
123 mt = 'text/plain'
123 mt = 'text/plain'
124 if mt.startswith('text/'):
124 if mt.startswith('text/'):
125 mt += '; charset="%s"' % encoding.encoding
125 mt += '; charset="%s"' % encoding.encoding
126
126
127 req.respond(HTTP_OK, mt, path, body=text)
127 req.respond(HTTP_OK, mt, path, body=text)
128 return []
128 return []
129
129
130 def _filerevision(web, req, tmpl, fctx):
130 def _filerevision(web, req, tmpl, fctx):
131 f = fctx.path()
131 f = fctx.path()
132 text = fctx.data()
132 text = fctx.data()
133 parity = paritygen(web.stripecount)
133 parity = paritygen(web.stripecount)
134 ishead = fctx.filerev() in fctx.filelog().headrevs()
134 ishead = fctx.filerev() in fctx.filelog().headrevs()
135
135
136 if util.binary(text):
136 if util.binary(text):
137 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
137 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
138 text = '(binary:%s)' % mt
138 text = '(binary:%s)' % mt
139
139
140 def lines():
140 def lines():
141 for lineno, t in enumerate(text.splitlines(True)):
141 for lineno, t in enumerate(text.splitlines(True)):
142 yield {"line": t,
142 yield {"line": t,
143 "lineid": "l%d" % (lineno + 1),
143 "lineid": "l%d" % (lineno + 1),
144 "linenumber": "% 6d" % (lineno + 1),
144 "linenumber": "% 6d" % (lineno + 1),
145 "parity": next(parity)}
145 "parity": next(parity)}
146
146
147 return tmpl("filerevision",
147 return tmpl("filerevision",
148 file=f,
148 file=f,
149 path=webutil.up(f),
149 path=webutil.up(f),
150 text=lines(),
150 text=lines(),
151 symrev=webutil.symrevorshortnode(req, fctx),
151 symrev=webutil.symrevorshortnode(req, fctx),
152 rename=webutil.renamelink(fctx),
152 rename=webutil.renamelink(fctx),
153 permissions=fctx.manifest().flags(f),
153 permissions=fctx.manifest().flags(f),
154 ishead=int(ishead),
154 ishead=int(ishead),
155 **webutil.commonentry(web.repo, fctx))
155 **webutil.commonentry(web.repo, fctx))
156
156
157 @webcommand('file')
157 @webcommand('file')
158 def file(web, req, tmpl):
158 def file(web, req, tmpl):
159 """
159 """
160 /file/{revision}[/{path}]
160 /file/{revision}[/{path}]
161 -------------------------
161 -------------------------
162
162
163 Show information about a directory or file in the repository.
163 Show information about a directory or file in the repository.
164
164
165 Info about the ``path`` given as a URL parameter will be rendered.
165 Info about the ``path`` given as a URL parameter will be rendered.
166
166
167 If ``path`` is a directory, information about the entries in that
167 If ``path`` is a directory, information about the entries in that
168 directory will be rendered. This form is equivalent to the ``manifest``
168 directory will be rendered. This form is equivalent to the ``manifest``
169 handler.
169 handler.
170
170
171 If ``path`` is a file, information about that file will be shown via
171 If ``path`` is a file, information about that file will be shown via
172 the ``filerevision`` template.
172 the ``filerevision`` template.
173
173
174 If ``path`` is not defined, information about the root directory will
174 If ``path`` is not defined, information about the root directory will
175 be rendered.
175 be rendered.
176 """
176 """
177 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
177 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
178 if not path:
178 if not path:
179 return manifest(web, req, tmpl)
179 return manifest(web, req, tmpl)
180 try:
180 try:
181 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
181 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
182 except error.LookupError as inst:
182 except error.LookupError as inst:
183 try:
183 try:
184 return manifest(web, req, tmpl)
184 return manifest(web, req, tmpl)
185 except ErrorResponse:
185 except ErrorResponse:
186 raise inst
186 raise inst
187
187
188 def _search(web, req, tmpl):
188 def _search(web, req, tmpl):
189 MODE_REVISION = 'rev'
189 MODE_REVISION = 'rev'
190 MODE_KEYWORD = 'keyword'
190 MODE_KEYWORD = 'keyword'
191 MODE_REVSET = 'revset'
191 MODE_REVSET = 'revset'
192
192
193 def revsearch(ctx):
193 def revsearch(ctx):
194 yield ctx
194 yield ctx
195
195
196 def keywordsearch(query):
196 def keywordsearch(query):
197 lower = encoding.lower
197 lower = encoding.lower
198 qw = lower(query).split()
198 qw = lower(query).split()
199
199
200 def revgen():
200 def revgen():
201 cl = web.repo.changelog
201 cl = web.repo.changelog
202 for i in xrange(len(web.repo) - 1, 0, -100):
202 for i in xrange(len(web.repo) - 1, 0, -100):
203 l = []
203 l = []
204 for j in cl.revs(max(0, i - 99), i):
204 for j in cl.revs(max(0, i - 99), i):
205 ctx = web.repo[j]
205 ctx = web.repo[j]
206 l.append(ctx)
206 l.append(ctx)
207 l.reverse()
207 l.reverse()
208 for e in l:
208 for e in l:
209 yield e
209 yield e
210
210
211 for ctx in revgen():
211 for ctx in revgen():
212 miss = 0
212 miss = 0
213 for q in qw:
213 for q in qw:
214 if not (q in lower(ctx.user()) or
214 if not (q in lower(ctx.user()) or
215 q in lower(ctx.description()) or
215 q in lower(ctx.description()) or
216 q in lower(" ".join(ctx.files()))):
216 q in lower(" ".join(ctx.files()))):
217 miss = 1
217 miss = 1
218 break
218 break
219 if miss:
219 if miss:
220 continue
220 continue
221
221
222 yield ctx
222 yield ctx
223
223
224 def revsetsearch(revs):
224 def revsetsearch(revs):
225 for r in revs:
225 for r in revs:
226 yield web.repo[r]
226 yield web.repo[r]
227
227
228 searchfuncs = {
228 searchfuncs = {
229 MODE_REVISION: (revsearch, 'exact revision search'),
229 MODE_REVISION: (revsearch, 'exact revision search'),
230 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
230 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
231 MODE_REVSET: (revsetsearch, 'revset expression search'),
231 MODE_REVSET: (revsetsearch, 'revset expression search'),
232 }
232 }
233
233
234 def getsearchmode(query):
234 def getsearchmode(query):
235 try:
235 try:
236 ctx = web.repo[query]
236 ctx = web.repo[query]
237 except (error.RepoError, error.LookupError):
237 except (error.RepoError, error.LookupError):
238 # query is not an exact revision pointer, need to
238 # query is not an exact revision pointer, need to
239 # decide if it's a revset expression or keywords
239 # decide if it's a revset expression or keywords
240 pass
240 pass
241 else:
241 else:
242 return MODE_REVISION, ctx
242 return MODE_REVISION, ctx
243
243
244 revdef = 'reverse(%s)' % query
244 revdef = 'reverse(%s)' % query
245 try:
245 try:
246 tree = revsetlang.parse(revdef)
246 tree = revsetlang.parse(revdef)
247 except error.ParseError:
247 except error.ParseError:
248 # can't parse to a revset tree
248 # can't parse to a revset tree
249 return MODE_KEYWORD, query
249 return MODE_KEYWORD, query
250
250
251 if revsetlang.depth(tree) <= 2:
251 if revsetlang.depth(tree) <= 2:
252 # no revset syntax used
252 # no revset syntax used
253 return MODE_KEYWORD, query
253 return MODE_KEYWORD, query
254
254
255 if any((token, (value or '')[:3]) == ('string', 're:')
255 if any((token, (value or '')[:3]) == ('string', 're:')
256 for token, value, pos in revsetlang.tokenize(revdef)):
256 for token, value, pos in revsetlang.tokenize(revdef)):
257 return MODE_KEYWORD, query
257 return MODE_KEYWORD, query
258
258
259 funcsused = revsetlang.funcsused(tree)
259 funcsused = revsetlang.funcsused(tree)
260 if not funcsused.issubset(revset.safesymbols):
260 if not funcsused.issubset(revset.safesymbols):
261 return MODE_KEYWORD, query
261 return MODE_KEYWORD, query
262
262
263 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
263 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
264 try:
264 try:
265 revs = mfunc(web.repo)
265 revs = mfunc(web.repo)
266 return MODE_REVSET, revs
266 return MODE_REVSET, revs
267 # ParseError: wrongly placed tokens, wrongs arguments, etc
267 # ParseError: wrongly placed tokens, wrongs arguments, etc
268 # RepoLookupError: no such revision, e.g. in 'revision:'
268 # RepoLookupError: no such revision, e.g. in 'revision:'
269 # Abort: bookmark/tag not exists
269 # Abort: bookmark/tag not exists
270 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
270 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
271 except (error.ParseError, error.RepoLookupError, error.Abort,
271 except (error.ParseError, error.RepoLookupError, error.Abort,
272 LookupError):
272 LookupError):
273 return MODE_KEYWORD, query
273 return MODE_KEYWORD, query
274
274
275 def changelist(**map):
275 def changelist(**map):
276 count = 0
276 count = 0
277
277
278 for ctx in searchfunc[0](funcarg):
278 for ctx in searchfunc[0](funcarg):
279 count += 1
279 count += 1
280 n = ctx.node()
280 n = ctx.node()
281 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
281 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
282 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
282 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
283
283
284 yield tmpl('searchentry',
284 yield tmpl('searchentry',
285 parity=next(parity),
285 parity=next(parity),
286 changelogtag=showtags,
286 changelogtag=showtags,
287 files=files,
287 files=files,
288 **webutil.commonentry(web.repo, ctx))
288 **webutil.commonentry(web.repo, ctx))
289
289
290 if count >= revcount:
290 if count >= revcount:
291 break
291 break
292
292
293 query = req.form['rev'][0]
293 query = req.form['rev'][0]
294 revcount = web.maxchanges
294 revcount = web.maxchanges
295 if 'revcount' in req.form:
295 if 'revcount' in req.form:
296 try:
296 try:
297 revcount = int(req.form.get('revcount', [revcount])[0])
297 revcount = int(req.form.get('revcount', [revcount])[0])
298 revcount = max(revcount, 1)
298 revcount = max(revcount, 1)
299 tmpl.defaults['sessionvars']['revcount'] = revcount
299 tmpl.defaults['sessionvars']['revcount'] = revcount
300 except ValueError:
300 except ValueError:
301 pass
301 pass
302
302
303 lessvars = copy.copy(tmpl.defaults['sessionvars'])
303 lessvars = copy.copy(tmpl.defaults['sessionvars'])
304 lessvars['revcount'] = max(revcount / 2, 1)
304 lessvars['revcount'] = max(revcount / 2, 1)
305 lessvars['rev'] = query
305 lessvars['rev'] = query
306 morevars = copy.copy(tmpl.defaults['sessionvars'])
306 morevars = copy.copy(tmpl.defaults['sessionvars'])
307 morevars['revcount'] = revcount * 2
307 morevars['revcount'] = revcount * 2
308 morevars['rev'] = query
308 morevars['rev'] = query
309
309
310 mode, funcarg = getsearchmode(query)
310 mode, funcarg = getsearchmode(query)
311
311
312 if 'forcekw' in req.form:
312 if 'forcekw' in req.form:
313 showforcekw = ''
313 showforcekw = ''
314 showunforcekw = searchfuncs[mode][1]
314 showunforcekw = searchfuncs[mode][1]
315 mode = MODE_KEYWORD
315 mode = MODE_KEYWORD
316 funcarg = query
316 funcarg = query
317 else:
317 else:
318 if mode != MODE_KEYWORD:
318 if mode != MODE_KEYWORD:
319 showforcekw = searchfuncs[MODE_KEYWORD][1]
319 showforcekw = searchfuncs[MODE_KEYWORD][1]
320 else:
320 else:
321 showforcekw = ''
321 showforcekw = ''
322 showunforcekw = ''
322 showunforcekw = ''
323
323
324 searchfunc = searchfuncs[mode]
324 searchfunc = searchfuncs[mode]
325
325
326 tip = web.repo['tip']
326 tip = web.repo['tip']
327 parity = paritygen(web.stripecount)
327 parity = paritygen(web.stripecount)
328
328
329 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
329 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
330 entries=changelist, archives=web.archivelist("tip"),
330 entries=changelist, archives=web.archivelist("tip"),
331 morevars=morevars, lessvars=lessvars,
331 morevars=morevars, lessvars=lessvars,
332 modedesc=searchfunc[1],
332 modedesc=searchfunc[1],
333 showforcekw=showforcekw, showunforcekw=showunforcekw)
333 showforcekw=showforcekw, showunforcekw=showunforcekw)
334
334
335 @webcommand('changelog')
335 @webcommand('changelog')
336 def changelog(web, req, tmpl, shortlog=False):
336 def changelog(web, req, tmpl, shortlog=False):
337 """
337 """
338 /changelog[/{revision}]
338 /changelog[/{revision}]
339 -----------------------
339 -----------------------
340
340
341 Show information about multiple changesets.
341 Show information about multiple changesets.
342
342
343 If the optional ``revision`` URL argument is absent, information about
343 If the optional ``revision`` URL argument is absent, information about
344 all changesets starting at ``tip`` will be rendered. If the ``revision``
344 all changesets starting at ``tip`` will be rendered. If the ``revision``
345 argument is present, changesets will be shown starting from the specified
345 argument is present, changesets will be shown starting from the specified
346 revision.
346 revision.
347
347
348 If ``revision`` is absent, the ``rev`` query string argument may be
348 If ``revision`` is absent, the ``rev`` query string argument may be
349 defined. This will perform a search for changesets.
349 defined. This will perform a search for changesets.
350
350
351 The argument for ``rev`` can be a single revision, a revision set,
351 The argument for ``rev`` can be a single revision, a revision set,
352 or a literal keyword to search for in changeset data (equivalent to
352 or a literal keyword to search for in changeset data (equivalent to
353 :hg:`log -k`).
353 :hg:`log -k`).
354
354
355 The ``revcount`` query string argument defines the maximum numbers of
355 The ``revcount`` query string argument defines the maximum numbers of
356 changesets to render.
356 changesets to render.
357
357
358 For non-searches, the ``changelog`` template will be rendered.
358 For non-searches, the ``changelog`` template will be rendered.
359 """
359 """
360
360
361 query = ''
361 query = ''
362 if 'node' in req.form:
362 if 'node' in req.form:
363 ctx = webutil.changectx(web.repo, req)
363 ctx = webutil.changectx(web.repo, req)
364 symrev = webutil.symrevorshortnode(req, ctx)
364 symrev = webutil.symrevorshortnode(req, ctx)
365 elif 'rev' in req.form:
365 elif 'rev' in req.form:
366 return _search(web, req, tmpl)
366 return _search(web, req, tmpl)
367 else:
367 else:
368 ctx = web.repo['tip']
368 ctx = web.repo['tip']
369 symrev = 'tip'
369 symrev = 'tip'
370
370
371 def changelist():
371 def changelist():
372 revs = []
372 revs = []
373 if pos != -1:
373 if pos != -1:
374 revs = web.repo.changelog.revs(pos, 0)
374 revs = web.repo.changelog.revs(pos, 0)
375 curcount = 0
375 curcount = 0
376 for rev in revs:
376 for rev in revs:
377 curcount += 1
377 curcount += 1
378 if curcount > revcount + 1:
378 if curcount > revcount + 1:
379 break
379 break
380
380
381 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
381 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
382 entry['parity'] = next(parity)
382 entry['parity'] = next(parity)
383 yield entry
383 yield entry
384
384
385 if shortlog:
385 if shortlog:
386 revcount = web.maxshortchanges
386 revcount = web.maxshortchanges
387 else:
387 else:
388 revcount = web.maxchanges
388 revcount = web.maxchanges
389
389
390 if 'revcount' in req.form:
390 if 'revcount' in req.form:
391 try:
391 try:
392 revcount = int(req.form.get('revcount', [revcount])[0])
392 revcount = int(req.form.get('revcount', [revcount])[0])
393 revcount = max(revcount, 1)
393 revcount = max(revcount, 1)
394 tmpl.defaults['sessionvars']['revcount'] = revcount
394 tmpl.defaults['sessionvars']['revcount'] = revcount
395 except ValueError:
395 except ValueError:
396 pass
396 pass
397
397
398 lessvars = copy.copy(tmpl.defaults['sessionvars'])
398 lessvars = copy.copy(tmpl.defaults['sessionvars'])
399 lessvars['revcount'] = max(revcount / 2, 1)
399 lessvars['revcount'] = max(revcount / 2, 1)
400 morevars = copy.copy(tmpl.defaults['sessionvars'])
400 morevars = copy.copy(tmpl.defaults['sessionvars'])
401 morevars['revcount'] = revcount * 2
401 morevars['revcount'] = revcount * 2
402
402
403 count = len(web.repo)
403 count = len(web.repo)
404 pos = ctx.rev()
404 pos = ctx.rev()
405 parity = paritygen(web.stripecount)
405 parity = paritygen(web.stripecount)
406
406
407 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
407 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
408
408
409 entries = list(changelist())
409 entries = list(changelist())
410 latestentry = entries[:1]
410 latestentry = entries[:1]
411 if len(entries) > revcount:
411 if len(entries) > revcount:
412 nextentry = entries[-1:]
412 nextentry = entries[-1:]
413 entries = entries[:-1]
413 entries = entries[:-1]
414 else:
414 else:
415 nextentry = []
415 nextentry = []
416
416
417 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
417 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
418 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
418 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
419 entries=entries,
419 entries=entries,
420 latestentry=latestentry, nextentry=nextentry,
420 latestentry=latestentry, nextentry=nextentry,
421 archives=web.archivelist("tip"), revcount=revcount,
421 archives=web.archivelist("tip"), revcount=revcount,
422 morevars=morevars, lessvars=lessvars, query=query)
422 morevars=morevars, lessvars=lessvars, query=query)
423
423
424 @webcommand('shortlog')
424 @webcommand('shortlog')
425 def shortlog(web, req, tmpl):
425 def shortlog(web, req, tmpl):
426 """
426 """
427 /shortlog
427 /shortlog
428 ---------
428 ---------
429
429
430 Show basic information about a set of changesets.
430 Show basic information about a set of changesets.
431
431
432 This accepts the same parameters as the ``changelog`` handler. The only
432 This accepts the same parameters as the ``changelog`` handler. The only
433 difference is the ``shortlog`` template will be rendered instead of the
433 difference is the ``shortlog`` template will be rendered instead of the
434 ``changelog`` template.
434 ``changelog`` template.
435 """
435 """
436 return changelog(web, req, tmpl, shortlog=True)
436 return changelog(web, req, tmpl, shortlog=True)
437
437
438 @webcommand('changeset')
438 @webcommand('changeset')
439 def changeset(web, req, tmpl):
439 def changeset(web, req, tmpl):
440 """
440 """
441 /changeset[/{revision}]
441 /changeset[/{revision}]
442 -----------------------
442 -----------------------
443
443
444 Show information about a single changeset.
444 Show information about a single changeset.
445
445
446 A URL path argument is the changeset identifier to show. See ``hg help
446 A URL path argument is the changeset identifier to show. See ``hg help
447 revisions`` for possible values. If not defined, the ``tip`` changeset
447 revisions`` for possible values. If not defined, the ``tip`` changeset
448 will be shown.
448 will be shown.
449
449
450 The ``changeset`` template is rendered. Contents of the ``changesettag``,
450 The ``changeset`` template is rendered. Contents of the ``changesettag``,
451 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
451 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
452 templates related to diffs may all be used to produce the output.
452 templates related to diffs may all be used to produce the output.
453 """
453 """
454 ctx = webutil.changectx(web.repo, req)
454 ctx = webutil.changectx(web.repo, req)
455
455
456 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
456 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
457
457
458 rev = webcommand('rev')(changeset)
458 rev = webcommand('rev')(changeset)
459
459
460 def decodepath(path):
460 def decodepath(path):
461 """Hook for mapping a path in the repository to a path in the
461 """Hook for mapping a path in the repository to a path in the
462 working copy.
462 working copy.
463
463
464 Extensions (e.g., largefiles) can override this to remap files in
464 Extensions (e.g., largefiles) can override this to remap files in
465 the virtual file system presented by the manifest command below."""
465 the virtual file system presented by the manifest command below."""
466 return path
466 return path
467
467
468 @webcommand('manifest')
468 @webcommand('manifest')
469 def manifest(web, req, tmpl):
469 def manifest(web, req, tmpl):
470 """
470 """
471 /manifest[/{revision}[/{path}]]
471 /manifest[/{revision}[/{path}]]
472 -------------------------------
472 -------------------------------
473
473
474 Show information about a directory.
474 Show information about a directory.
475
475
476 If the URL path arguments are omitted, information about the root
476 If the URL path arguments are omitted, information about the root
477 directory for the ``tip`` changeset will be shown.
477 directory for the ``tip`` changeset will be shown.
478
478
479 Because this handler can only show information for directories, it
479 Because this handler can only show information for directories, it
480 is recommended to use the ``file`` handler instead, as it can handle both
480 is recommended to use the ``file`` handler instead, as it can handle both
481 directories and files.
481 directories and files.
482
482
483 The ``manifest`` template will be rendered for this handler.
483 The ``manifest`` template will be rendered for this handler.
484 """
484 """
485 if 'node' in req.form:
485 if 'node' in req.form:
486 ctx = webutil.changectx(web.repo, req)
486 ctx = webutil.changectx(web.repo, req)
487 symrev = webutil.symrevorshortnode(req, ctx)
487 symrev = webutil.symrevorshortnode(req, ctx)
488 else:
488 else:
489 ctx = web.repo['tip']
489 ctx = web.repo['tip']
490 symrev = 'tip'
490 symrev = 'tip'
491 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
491 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
492 mf = ctx.manifest()
492 mf = ctx.manifest()
493 node = ctx.node()
493 node = ctx.node()
494
494
495 files = {}
495 files = {}
496 dirs = {}
496 dirs = {}
497 parity = paritygen(web.stripecount)
497 parity = paritygen(web.stripecount)
498
498
499 if path and path[-1] != "/":
499 if path and path[-1] != "/":
500 path += "/"
500 path += "/"
501 l = len(path)
501 l = len(path)
502 abspath = "/" + path
502 abspath = "/" + path
503
503
504 for full, n in mf.iteritems():
504 for full, n in mf.iteritems():
505 # the virtual path (working copy path) used for the full
505 # the virtual path (working copy path) used for the full
506 # (repository) path
506 # (repository) path
507 f = decodepath(full)
507 f = decodepath(full)
508
508
509 if f[:l] != path:
509 if f[:l] != path:
510 continue
510 continue
511 remain = f[l:]
511 remain = f[l:]
512 elements = remain.split('/')
512 elements = remain.split('/')
513 if len(elements) == 1:
513 if len(elements) == 1:
514 files[remain] = full
514 files[remain] = full
515 else:
515 else:
516 h = dirs # need to retain ref to dirs (root)
516 h = dirs # need to retain ref to dirs (root)
517 for elem in elements[0:-1]:
517 for elem in elements[0:-1]:
518 if elem not in h:
518 if elem not in h:
519 h[elem] = {}
519 h[elem] = {}
520 h = h[elem]
520 h = h[elem]
521 if len(h) > 1:
521 if len(h) > 1:
522 break
522 break
523 h[None] = None # denotes files present
523 h[None] = None # denotes files present
524
524
525 if mf and not files and not dirs:
525 if mf and not files and not dirs:
526 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
526 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
527
527
528 def filelist(**map):
528 def filelist(**map):
529 for f in sorted(files):
529 for f in sorted(files):
530 full = files[f]
530 full = files[f]
531
531
532 fctx = ctx.filectx(full)
532 fctx = ctx.filectx(full)
533 yield {"file": full,
533 yield {"file": full,
534 "parity": next(parity),
534 "parity": next(parity),
535 "basename": f,
535 "basename": f,
536 "date": fctx.date(),
536 "date": fctx.date(),
537 "size": fctx.size(),
537 "size": fctx.size(),
538 "permissions": mf.flags(full)}
538 "permissions": mf.flags(full)}
539
539
540 def dirlist(**map):
540 def dirlist(**map):
541 for d in sorted(dirs):
541 for d in sorted(dirs):
542
542
543 emptydirs = []
543 emptydirs = []
544 h = dirs[d]
544 h = dirs[d]
545 while isinstance(h, dict) and len(h) == 1:
545 while isinstance(h, dict) and len(h) == 1:
546 k, v = h.items()[0]
546 k, v = h.items()[0]
547 if v:
547 if v:
548 emptydirs.append(k)
548 emptydirs.append(k)
549 h = v
549 h = v
550
550
551 path = "%s%s" % (abspath, d)
551 path = "%s%s" % (abspath, d)
552 yield {"parity": next(parity),
552 yield {"parity": next(parity),
553 "path": path,
553 "path": path,
554 "emptydirs": "/".join(emptydirs),
554 "emptydirs": "/".join(emptydirs),
555 "basename": d}
555 "basename": d}
556
556
557 return tmpl("manifest",
557 return tmpl("manifest",
558 symrev=symrev,
558 symrev=symrev,
559 path=abspath,
559 path=abspath,
560 up=webutil.up(abspath),
560 up=webutil.up(abspath),
561 upparity=next(parity),
561 upparity=next(parity),
562 fentries=filelist,
562 fentries=filelist,
563 dentries=dirlist,
563 dentries=dirlist,
564 archives=web.archivelist(hex(node)),
564 archives=web.archivelist(hex(node)),
565 **webutil.commonentry(web.repo, ctx))
565 **webutil.commonentry(web.repo, ctx))
566
566
567 @webcommand('tags')
567 @webcommand('tags')
568 def tags(web, req, tmpl):
568 def tags(web, req, tmpl):
569 """
569 """
570 /tags
570 /tags
571 -----
571 -----
572
572
573 Show information about tags.
573 Show information about tags.
574
574
575 No arguments are accepted.
575 No arguments are accepted.
576
576
577 The ``tags`` template is rendered.
577 The ``tags`` template is rendered.
578 """
578 """
579 i = list(reversed(web.repo.tagslist()))
579 i = list(reversed(web.repo.tagslist()))
580 parity = paritygen(web.stripecount)
580 parity = paritygen(web.stripecount)
581
581
582 def entries(notip, latestonly, **map):
582 def entries(notip, latestonly, **map):
583 t = i
583 t = i
584 if notip:
584 if notip:
585 t = [(k, n) for k, n in i if k != "tip"]
585 t = [(k, n) for k, n in i if k != "tip"]
586 if latestonly:
586 if latestonly:
587 t = t[:1]
587 t = t[:1]
588 for k, n in t:
588 for k, n in t:
589 yield {"parity": next(parity),
589 yield {"parity": next(parity),
590 "tag": k,
590 "tag": k,
591 "date": web.repo[n].date(),
591 "date": web.repo[n].date(),
592 "node": hex(n)}
592 "node": hex(n)}
593
593
594 return tmpl("tags",
594 return tmpl("tags",
595 node=hex(web.repo.changelog.tip()),
595 node=hex(web.repo.changelog.tip()),
596 entries=lambda **x: entries(False, False, **x),
596 entries=lambda **x: entries(False, False, **x),
597 entriesnotip=lambda **x: entries(True, False, **x),
597 entriesnotip=lambda **x: entries(True, False, **x),
598 latestentry=lambda **x: entries(True, True, **x))
598 latestentry=lambda **x: entries(True, True, **x))
599
599
600 @webcommand('bookmarks')
600 @webcommand('bookmarks')
601 def bookmarks(web, req, tmpl):
601 def bookmarks(web, req, tmpl):
602 """
602 """
603 /bookmarks
603 /bookmarks
604 ----------
604 ----------
605
605
606 Show information about bookmarks.
606 Show information about bookmarks.
607
607
608 No arguments are accepted.
608 No arguments are accepted.
609
609
610 The ``bookmarks`` template is rendered.
610 The ``bookmarks`` template is rendered.
611 """
611 """
612 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
612 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
613 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
613 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
614 i = sorted(i, key=sortkey, reverse=True)
614 i = sorted(i, key=sortkey, reverse=True)
615 parity = paritygen(web.stripecount)
615 parity = paritygen(web.stripecount)
616
616
617 def entries(latestonly, **map):
617 def entries(latestonly, **map):
618 t = i
618 t = i
619 if latestonly:
619 if latestonly:
620 t = i[:1]
620 t = i[:1]
621 for k, n in t:
621 for k, n in t:
622 yield {"parity": next(parity),
622 yield {"parity": next(parity),
623 "bookmark": k,
623 "bookmark": k,
624 "date": web.repo[n].date(),
624 "date": web.repo[n].date(),
625 "node": hex(n)}
625 "node": hex(n)}
626
626
627 if i:
627 if i:
628 latestrev = i[0][1]
628 latestrev = i[0][1]
629 else:
629 else:
630 latestrev = -1
630 latestrev = -1
631
631
632 return tmpl("bookmarks",
632 return tmpl("bookmarks",
633 node=hex(web.repo.changelog.tip()),
633 node=hex(web.repo.changelog.tip()),
634 lastchange=[{"date": web.repo[latestrev].date()}],
634 lastchange=[{"date": web.repo[latestrev].date()}],
635 entries=lambda **x: entries(latestonly=False, **x),
635 entries=lambda **x: entries(latestonly=False, **x),
636 latestentry=lambda **x: entries(latestonly=True, **x))
636 latestentry=lambda **x: entries(latestonly=True, **x))
637
637
638 @webcommand('branches')
638 @webcommand('branches')
639 def branches(web, req, tmpl):
639 def branches(web, req, tmpl):
640 """
640 """
641 /branches
641 /branches
642 ---------
642 ---------
643
643
644 Show information about branches.
644 Show information about branches.
645
645
646 All known branches are contained in the output, even closed branches.
646 All known branches are contained in the output, even closed branches.
647
647
648 No arguments are accepted.
648 No arguments are accepted.
649
649
650 The ``branches`` template is rendered.
650 The ``branches`` template is rendered.
651 """
651 """
652 entries = webutil.branchentries(web.repo, web.stripecount)
652 entries = webutil.branchentries(web.repo, web.stripecount)
653 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
653 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
654 return tmpl('branches', node=hex(web.repo.changelog.tip()),
654 return tmpl('branches', node=hex(web.repo.changelog.tip()),
655 entries=entries, latestentry=latestentry)
655 entries=entries, latestentry=latestentry)
656
656
657 @webcommand('summary')
657 @webcommand('summary')
658 def summary(web, req, tmpl):
658 def summary(web, req, tmpl):
659 """
659 """
660 /summary
660 /summary
661 --------
661 --------
662
662
663 Show a summary of repository state.
663 Show a summary of repository state.
664
664
665 Information about the latest changesets, bookmarks, tags, and branches
665 Information about the latest changesets, bookmarks, tags, and branches
666 is captured by this handler.
666 is captured by this handler.
667
667
668 The ``summary`` template is rendered.
668 The ``summary`` template is rendered.
669 """
669 """
670 i = reversed(web.repo.tagslist())
670 i = reversed(web.repo.tagslist())
671
671
672 def tagentries(**map):
672 def tagentries(**map):
673 parity = paritygen(web.stripecount)
673 parity = paritygen(web.stripecount)
674 count = 0
674 count = 0
675 for k, n in i:
675 for k, n in i:
676 if k == "tip": # skip tip
676 if k == "tip": # skip tip
677 continue
677 continue
678
678
679 count += 1
679 count += 1
680 if count > 10: # limit to 10 tags
680 if count > 10: # limit to 10 tags
681 break
681 break
682
682
683 yield tmpl("tagentry",
683 yield tmpl("tagentry",
684 parity=next(parity),
684 parity=next(parity),
685 tag=k,
685 tag=k,
686 node=hex(n),
686 node=hex(n),
687 date=web.repo[n].date())
687 date=web.repo[n].date())
688
688
689 def bookmarks(**map):
689 def bookmarks(**map):
690 parity = paritygen(web.stripecount)
690 parity = paritygen(web.stripecount)
691 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
691 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
692 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
692 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
693 marks = sorted(marks, key=sortkey, reverse=True)
693 marks = sorted(marks, key=sortkey, reverse=True)
694 for k, n in marks[:10]: # limit to 10 bookmarks
694 for k, n in marks[:10]: # limit to 10 bookmarks
695 yield {'parity': next(parity),
695 yield {'parity': next(parity),
696 'bookmark': k,
696 'bookmark': k,
697 'date': web.repo[n].date(),
697 'date': web.repo[n].date(),
698 'node': hex(n)}
698 'node': hex(n)}
699
699
700 def changelist(**map):
700 def changelist(**map):
701 parity = paritygen(web.stripecount, offset=start - end)
701 parity = paritygen(web.stripecount, offset=start - end)
702 l = [] # build a list in forward order for efficiency
702 l = [] # build a list in forward order for efficiency
703 revs = []
703 revs = []
704 if start < end:
704 if start < end:
705 revs = web.repo.changelog.revs(start, end - 1)
705 revs = web.repo.changelog.revs(start, end - 1)
706 for i in revs:
706 for i in revs:
707 ctx = web.repo[i]
707 ctx = web.repo[i]
708
708
709 l.append(tmpl(
709 l.append(tmpl(
710 'shortlogentry',
710 'shortlogentry',
711 parity=next(parity),
711 parity=next(parity),
712 **webutil.commonentry(web.repo, ctx)))
712 **webutil.commonentry(web.repo, ctx)))
713
713
714 for entry in reversed(l):
714 for entry in reversed(l):
715 yield entry
715 yield entry
716
716
717 tip = web.repo['tip']
717 tip = web.repo['tip']
718 count = len(web.repo)
718 count = len(web.repo)
719 start = max(0, count - web.maxchanges)
719 start = max(0, count - web.maxchanges)
720 end = min(count, start + web.maxchanges)
720 end = min(count, start + web.maxchanges)
721
721
722 desc = web.config("web", "description")
723 if not desc:
724 desc = 'unknown'
722 return tmpl("summary",
725 return tmpl("summary",
723 desc=web.config("web", "description", "unknown"),
726 desc=desc,
724 owner=get_contact(web.config) or "unknown",
727 owner=get_contact(web.config) or "unknown",
725 lastchange=tip.date(),
728 lastchange=tip.date(),
726 tags=tagentries,
729 tags=tagentries,
727 bookmarks=bookmarks,
730 bookmarks=bookmarks,
728 branches=webutil.branchentries(web.repo, web.stripecount, 10),
731 branches=webutil.branchentries(web.repo, web.stripecount, 10),
729 shortlog=changelist,
732 shortlog=changelist,
730 node=tip.hex(),
733 node=tip.hex(),
731 symrev='tip',
734 symrev='tip',
732 archives=web.archivelist("tip"),
735 archives=web.archivelist("tip"),
733 labels=web.configlist('web', 'labels'))
736 labels=web.configlist('web', 'labels'))
734
737
735 @webcommand('filediff')
738 @webcommand('filediff')
736 def filediff(web, req, tmpl):
739 def filediff(web, req, tmpl):
737 """
740 """
738 /diff/{revision}/{path}
741 /diff/{revision}/{path}
739 -----------------------
742 -----------------------
740
743
741 Show how a file changed in a particular commit.
744 Show how a file changed in a particular commit.
742
745
743 The ``filediff`` template is rendered.
746 The ``filediff`` template is rendered.
744
747
745 This handler is registered under both the ``/diff`` and ``/filediff``
748 This handler is registered under both the ``/diff`` and ``/filediff``
746 paths. ``/diff`` is used in modern code.
749 paths. ``/diff`` is used in modern code.
747 """
750 """
748 fctx, ctx = None, None
751 fctx, ctx = None, None
749 try:
752 try:
750 fctx = webutil.filectx(web.repo, req)
753 fctx = webutil.filectx(web.repo, req)
751 except LookupError:
754 except LookupError:
752 ctx = webutil.changectx(web.repo, req)
755 ctx = webutil.changectx(web.repo, req)
753 path = webutil.cleanpath(web.repo, req.form['file'][0])
756 path = webutil.cleanpath(web.repo, req.form['file'][0])
754 if path not in ctx.files():
757 if path not in ctx.files():
755 raise
758 raise
756
759
757 if fctx is not None:
760 if fctx is not None:
758 path = fctx.path()
761 path = fctx.path()
759 ctx = fctx.changectx()
762 ctx = fctx.changectx()
760 basectx = ctx.p1()
763 basectx = ctx.p1()
761
764
762 style = web.config('web', 'style', 'paper')
765 style = web.config('web', 'style', 'paper')
763 if 'style' in req.form:
766 if 'style' in req.form:
764 style = req.form['style'][0]
767 style = req.form['style'][0]
765
768
766 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
769 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
767 if fctx is not None:
770 if fctx is not None:
768 rename = webutil.renamelink(fctx)
771 rename = webutil.renamelink(fctx)
769 ctx = fctx
772 ctx = fctx
770 else:
773 else:
771 rename = []
774 rename = []
772 ctx = ctx
775 ctx = ctx
773 return tmpl("filediff",
776 return tmpl("filediff",
774 file=path,
777 file=path,
775 symrev=webutil.symrevorshortnode(req, ctx),
778 symrev=webutil.symrevorshortnode(req, ctx),
776 rename=rename,
779 rename=rename,
777 diff=diffs,
780 diff=diffs,
778 **webutil.commonentry(web.repo, ctx))
781 **webutil.commonentry(web.repo, ctx))
779
782
780 diff = webcommand('diff')(filediff)
783 diff = webcommand('diff')(filediff)
781
784
782 @webcommand('comparison')
785 @webcommand('comparison')
783 def comparison(web, req, tmpl):
786 def comparison(web, req, tmpl):
784 """
787 """
785 /comparison/{revision}/{path}
788 /comparison/{revision}/{path}
786 -----------------------------
789 -----------------------------
787
790
788 Show a comparison between the old and new versions of a file from changes
791 Show a comparison between the old and new versions of a file from changes
789 made on a particular revision.
792 made on a particular revision.
790
793
791 This is similar to the ``diff`` handler. However, this form features
794 This is similar to the ``diff`` handler. However, this form features
792 a split or side-by-side diff rather than a unified diff.
795 a split or side-by-side diff rather than a unified diff.
793
796
794 The ``context`` query string argument can be used to control the lines of
797 The ``context`` query string argument can be used to control the lines of
795 context in the diff.
798 context in the diff.
796
799
797 The ``filecomparison`` template is rendered.
800 The ``filecomparison`` template is rendered.
798 """
801 """
799 ctx = webutil.changectx(web.repo, req)
802 ctx = webutil.changectx(web.repo, req)
800 if 'file' not in req.form:
803 if 'file' not in req.form:
801 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
804 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
802 path = webutil.cleanpath(web.repo, req.form['file'][0])
805 path = webutil.cleanpath(web.repo, req.form['file'][0])
803
806
804 parsecontext = lambda v: v == 'full' and -1 or int(v)
807 parsecontext = lambda v: v == 'full' and -1 or int(v)
805 if 'context' in req.form:
808 if 'context' in req.form:
806 context = parsecontext(req.form['context'][0])
809 context = parsecontext(req.form['context'][0])
807 else:
810 else:
808 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
811 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
809
812
810 def filelines(f):
813 def filelines(f):
811 if f.isbinary():
814 if f.isbinary():
812 mt = mimetypes.guess_type(f.path())[0]
815 mt = mimetypes.guess_type(f.path())[0]
813 if not mt:
816 if not mt:
814 mt = 'application/octet-stream'
817 mt = 'application/octet-stream'
815 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
818 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
816 return f.data().splitlines()
819 return f.data().splitlines()
817
820
818 fctx = None
821 fctx = None
819 parent = ctx.p1()
822 parent = ctx.p1()
820 leftrev = parent.rev()
823 leftrev = parent.rev()
821 leftnode = parent.node()
824 leftnode = parent.node()
822 rightrev = ctx.rev()
825 rightrev = ctx.rev()
823 rightnode = ctx.node()
826 rightnode = ctx.node()
824 if path in ctx:
827 if path in ctx:
825 fctx = ctx[path]
828 fctx = ctx[path]
826 rightlines = filelines(fctx)
829 rightlines = filelines(fctx)
827 if path not in parent:
830 if path not in parent:
828 leftlines = ()
831 leftlines = ()
829 else:
832 else:
830 pfctx = parent[path]
833 pfctx = parent[path]
831 leftlines = filelines(pfctx)
834 leftlines = filelines(pfctx)
832 else:
835 else:
833 rightlines = ()
836 rightlines = ()
834 pfctx = ctx.parents()[0][path]
837 pfctx = ctx.parents()[0][path]
835 leftlines = filelines(pfctx)
838 leftlines = filelines(pfctx)
836
839
837 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
840 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
838 if fctx is not None:
841 if fctx is not None:
839 rename = webutil.renamelink(fctx)
842 rename = webutil.renamelink(fctx)
840 ctx = fctx
843 ctx = fctx
841 else:
844 else:
842 rename = []
845 rename = []
843 ctx = ctx
846 ctx = ctx
844 return tmpl('filecomparison',
847 return tmpl('filecomparison',
845 file=path,
848 file=path,
846 symrev=webutil.symrevorshortnode(req, ctx),
849 symrev=webutil.symrevorshortnode(req, ctx),
847 rename=rename,
850 rename=rename,
848 leftrev=leftrev,
851 leftrev=leftrev,
849 leftnode=hex(leftnode),
852 leftnode=hex(leftnode),
850 rightrev=rightrev,
853 rightrev=rightrev,
851 rightnode=hex(rightnode),
854 rightnode=hex(rightnode),
852 comparison=comparison,
855 comparison=comparison,
853 **webutil.commonentry(web.repo, ctx))
856 **webutil.commonentry(web.repo, ctx))
854
857
855 @webcommand('annotate')
858 @webcommand('annotate')
856 def annotate(web, req, tmpl):
859 def annotate(web, req, tmpl):
857 """
860 """
858 /annotate/{revision}/{path}
861 /annotate/{revision}/{path}
859 ---------------------------
862 ---------------------------
860
863
861 Show changeset information for each line in a file.
864 Show changeset information for each line in a file.
862
865
863 The ``fileannotate`` template is rendered.
866 The ``fileannotate`` template is rendered.
864 """
867 """
865 fctx = webutil.filectx(web.repo, req)
868 fctx = webutil.filectx(web.repo, req)
866 f = fctx.path()
869 f = fctx.path()
867 parity = paritygen(web.stripecount)
870 parity = paritygen(web.stripecount)
868 ishead = fctx.filerev() in fctx.filelog().headrevs()
871 ishead = fctx.filerev() in fctx.filelog().headrevs()
869
872
870 # parents() is called once per line and several lines likely belong to
873 # parents() is called once per line and several lines likely belong to
871 # same revision. So it is worth caching.
874 # same revision. So it is worth caching.
872 # TODO there are still redundant operations within basefilectx.parents()
875 # TODO there are still redundant operations within basefilectx.parents()
873 # and from the fctx.annotate() call itself that could be cached.
876 # and from the fctx.annotate() call itself that could be cached.
874 parentscache = {}
877 parentscache = {}
875 def parents(f):
878 def parents(f):
876 rev = f.rev()
879 rev = f.rev()
877 if rev not in parentscache:
880 if rev not in parentscache:
878 parentscache[rev] = []
881 parentscache[rev] = []
879 for p in f.parents():
882 for p in f.parents():
880 entry = {
883 entry = {
881 'node': p.hex(),
884 'node': p.hex(),
882 'rev': p.rev(),
885 'rev': p.rev(),
883 }
886 }
884 parentscache[rev].append(entry)
887 parentscache[rev].append(entry)
885
888
886 for p in parentscache[rev]:
889 for p in parentscache[rev]:
887 yield p
890 yield p
888
891
889 def annotate(**map):
892 def annotate(**map):
890 if fctx.isbinary():
893 if fctx.isbinary():
891 mt = (mimetypes.guess_type(fctx.path())[0]
894 mt = (mimetypes.guess_type(fctx.path())[0]
892 or 'application/octet-stream')
895 or 'application/octet-stream')
893 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
896 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
894 else:
897 else:
895 lines = webutil.annotate(fctx, web.repo.ui)
898 lines = webutil.annotate(fctx, web.repo.ui)
896
899
897 previousrev = None
900 previousrev = None
898 blockparitygen = paritygen(1)
901 blockparitygen = paritygen(1)
899 for lineno, ((f, targetline), l) in enumerate(lines):
902 for lineno, ((f, targetline), l) in enumerate(lines):
900 rev = f.rev()
903 rev = f.rev()
901 if rev != previousrev:
904 if rev != previousrev:
902 blockhead = True
905 blockhead = True
903 blockparity = next(blockparitygen)
906 blockparity = next(blockparitygen)
904 else:
907 else:
905 blockhead = None
908 blockhead = None
906 previousrev = rev
909 previousrev = rev
907 yield {"parity": next(parity),
910 yield {"parity": next(parity),
908 "node": f.hex(),
911 "node": f.hex(),
909 "rev": rev,
912 "rev": rev,
910 "author": f.user(),
913 "author": f.user(),
911 "parents": parents(f),
914 "parents": parents(f),
912 "desc": f.description(),
915 "desc": f.description(),
913 "extra": f.extra(),
916 "extra": f.extra(),
914 "file": f.path(),
917 "file": f.path(),
915 "blockhead": blockhead,
918 "blockhead": blockhead,
916 "blockparity": blockparity,
919 "blockparity": blockparity,
917 "targetline": targetline,
920 "targetline": targetline,
918 "line": l,
921 "line": l,
919 "lineno": lineno + 1,
922 "lineno": lineno + 1,
920 "lineid": "l%d" % (lineno + 1),
923 "lineid": "l%d" % (lineno + 1),
921 "linenumber": "% 6d" % (lineno + 1),
924 "linenumber": "% 6d" % (lineno + 1),
922 "revdate": f.date()}
925 "revdate": f.date()}
923
926
924 return tmpl("fileannotate",
927 return tmpl("fileannotate",
925 file=f,
928 file=f,
926 annotate=annotate,
929 annotate=annotate,
927 path=webutil.up(f),
930 path=webutil.up(f),
928 symrev=webutil.symrevorshortnode(req, fctx),
931 symrev=webutil.symrevorshortnode(req, fctx),
929 rename=webutil.renamelink(fctx),
932 rename=webutil.renamelink(fctx),
930 permissions=fctx.manifest().flags(f),
933 permissions=fctx.manifest().flags(f),
931 ishead=int(ishead),
934 ishead=int(ishead),
932 **webutil.commonentry(web.repo, fctx))
935 **webutil.commonentry(web.repo, fctx))
933
936
934 @webcommand('filelog')
937 @webcommand('filelog')
935 def filelog(web, req, tmpl):
938 def filelog(web, req, tmpl):
936 """
939 """
937 /filelog/{revision}/{path}
940 /filelog/{revision}/{path}
938 --------------------------
941 --------------------------
939
942
940 Show information about the history of a file in the repository.
943 Show information about the history of a file in the repository.
941
944
942 The ``revcount`` query string argument can be defined to control the
945 The ``revcount`` query string argument can be defined to control the
943 maximum number of entries to show.
946 maximum number of entries to show.
944
947
945 The ``filelog`` template will be rendered.
948 The ``filelog`` template will be rendered.
946 """
949 """
947
950
948 try:
951 try:
949 fctx = webutil.filectx(web.repo, req)
952 fctx = webutil.filectx(web.repo, req)
950 f = fctx.path()
953 f = fctx.path()
951 fl = fctx.filelog()
954 fl = fctx.filelog()
952 except error.LookupError:
955 except error.LookupError:
953 f = webutil.cleanpath(web.repo, req.form['file'][0])
956 f = webutil.cleanpath(web.repo, req.form['file'][0])
954 fl = web.repo.file(f)
957 fl = web.repo.file(f)
955 numrevs = len(fl)
958 numrevs = len(fl)
956 if not numrevs: # file doesn't exist at all
959 if not numrevs: # file doesn't exist at all
957 raise
960 raise
958 rev = webutil.changectx(web.repo, req).rev()
961 rev = webutil.changectx(web.repo, req).rev()
959 first = fl.linkrev(0)
962 first = fl.linkrev(0)
960 if rev < first: # current rev is from before file existed
963 if rev < first: # current rev is from before file existed
961 raise
964 raise
962 frev = numrevs - 1
965 frev = numrevs - 1
963 while fl.linkrev(frev) > rev:
966 while fl.linkrev(frev) > rev:
964 frev -= 1
967 frev -= 1
965 fctx = web.repo.filectx(f, fl.linkrev(frev))
968 fctx = web.repo.filectx(f, fl.linkrev(frev))
966
969
967 revcount = web.maxshortchanges
970 revcount = web.maxshortchanges
968 if 'revcount' in req.form:
971 if 'revcount' in req.form:
969 try:
972 try:
970 revcount = int(req.form.get('revcount', [revcount])[0])
973 revcount = int(req.form.get('revcount', [revcount])[0])
971 revcount = max(revcount, 1)
974 revcount = max(revcount, 1)
972 tmpl.defaults['sessionvars']['revcount'] = revcount
975 tmpl.defaults['sessionvars']['revcount'] = revcount
973 except ValueError:
976 except ValueError:
974 pass
977 pass
975
978
976 lrange = webutil.linerange(req)
979 lrange = webutil.linerange(req)
977
980
978 lessvars = copy.copy(tmpl.defaults['sessionvars'])
981 lessvars = copy.copy(tmpl.defaults['sessionvars'])
979 lessvars['revcount'] = max(revcount / 2, 1)
982 lessvars['revcount'] = max(revcount / 2, 1)
980 morevars = copy.copy(tmpl.defaults['sessionvars'])
983 morevars = copy.copy(tmpl.defaults['sessionvars'])
981 morevars['revcount'] = revcount * 2
984 morevars['revcount'] = revcount * 2
982
985
983 patch = 'patch' in req.form
986 patch = 'patch' in req.form
984 if patch:
987 if patch:
985 lessvars['patch'] = morevars['patch'] = req.form['patch'][0]
988 lessvars['patch'] = morevars['patch'] = req.form['patch'][0]
986 descend = 'descend' in req.form
989 descend = 'descend' in req.form
987 if descend:
990 if descend:
988 lessvars['descend'] = morevars['descend'] = req.form['descend'][0]
991 lessvars['descend'] = morevars['descend'] = req.form['descend'][0]
989
992
990 count = fctx.filerev() + 1
993 count = fctx.filerev() + 1
991 start = max(0, count - revcount) # first rev on this page
994 start = max(0, count - revcount) # first rev on this page
992 end = min(count, start + revcount) # last rev on this page
995 end = min(count, start + revcount) # last rev on this page
993 parity = paritygen(web.stripecount, offset=start - end)
996 parity = paritygen(web.stripecount, offset=start - end)
994
997
995 repo = web.repo
998 repo = web.repo
996 revs = fctx.filelog().revs(start, end - 1)
999 revs = fctx.filelog().revs(start, end - 1)
997 entries = []
1000 entries = []
998
1001
999 diffstyle = web.config('web', 'style', 'paper')
1002 diffstyle = web.config('web', 'style', 'paper')
1000 if 'style' in req.form:
1003 if 'style' in req.form:
1001 diffstyle = req.form['style'][0]
1004 diffstyle = req.form['style'][0]
1002
1005
1003 def diff(fctx, linerange=None):
1006 def diff(fctx, linerange=None):
1004 ctx = fctx.changectx()
1007 ctx = fctx.changectx()
1005 basectx = ctx.p1()
1008 basectx = ctx.p1()
1006 path = fctx.path()
1009 path = fctx.path()
1007 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1010 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1008 linerange=linerange,
1011 linerange=linerange,
1009 lineidprefix='%s-' % ctx.hex()[:12])
1012 lineidprefix='%s-' % ctx.hex()[:12])
1010
1013
1011 linerange = None
1014 linerange = None
1012 if lrange is not None:
1015 if lrange is not None:
1013 linerange = webutil.formatlinerange(*lrange)
1016 linerange = webutil.formatlinerange(*lrange)
1014 # deactivate numeric nav links when linerange is specified as this
1017 # deactivate numeric nav links when linerange is specified as this
1015 # would required a dedicated "revnav" class
1018 # would required a dedicated "revnav" class
1016 nav = None
1019 nav = None
1017 if descend:
1020 if descend:
1018 it = dagop.blockdescendants(fctx, *lrange)
1021 it = dagop.blockdescendants(fctx, *lrange)
1019 else:
1022 else:
1020 it = dagop.blockancestors(fctx, *lrange)
1023 it = dagop.blockancestors(fctx, *lrange)
1021 for i, (c, lr) in enumerate(it, 1):
1024 for i, (c, lr) in enumerate(it, 1):
1022 diffs = None
1025 diffs = None
1023 if patch:
1026 if patch:
1024 diffs = diff(c, linerange=lr)
1027 diffs = diff(c, linerange=lr)
1025 # follow renames accross filtered (not in range) revisions
1028 # follow renames accross filtered (not in range) revisions
1026 path = c.path()
1029 path = c.path()
1027 entries.append(dict(
1030 entries.append(dict(
1028 parity=next(parity),
1031 parity=next(parity),
1029 filerev=c.rev(),
1032 filerev=c.rev(),
1030 file=path,
1033 file=path,
1031 diff=diffs,
1034 diff=diffs,
1032 linerange=webutil.formatlinerange(*lr),
1035 linerange=webutil.formatlinerange(*lr),
1033 **webutil.commonentry(repo, c)))
1036 **webutil.commonentry(repo, c)))
1034 if i == revcount:
1037 if i == revcount:
1035 break
1038 break
1036 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1039 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1037 morevars['linerange'] = lessvars['linerange']
1040 morevars['linerange'] = lessvars['linerange']
1038 else:
1041 else:
1039 for i in revs:
1042 for i in revs:
1040 iterfctx = fctx.filectx(i)
1043 iterfctx = fctx.filectx(i)
1041 diffs = None
1044 diffs = None
1042 if patch:
1045 if patch:
1043 diffs = diff(iterfctx)
1046 diffs = diff(iterfctx)
1044 entries.append(dict(
1047 entries.append(dict(
1045 parity=next(parity),
1048 parity=next(parity),
1046 filerev=i,
1049 filerev=i,
1047 file=f,
1050 file=f,
1048 diff=diffs,
1051 diff=diffs,
1049 rename=webutil.renamelink(iterfctx),
1052 rename=webutil.renamelink(iterfctx),
1050 **webutil.commonentry(repo, iterfctx)))
1053 **webutil.commonentry(repo, iterfctx)))
1051 entries.reverse()
1054 entries.reverse()
1052 revnav = webutil.filerevnav(web.repo, fctx.path())
1055 revnav = webutil.filerevnav(web.repo, fctx.path())
1053 nav = revnav.gen(end - 1, revcount, count)
1056 nav = revnav.gen(end - 1, revcount, count)
1054
1057
1055 latestentry = entries[:1]
1058 latestentry = entries[:1]
1056
1059
1057 return tmpl("filelog",
1060 return tmpl("filelog",
1058 file=f,
1061 file=f,
1059 nav=nav,
1062 nav=nav,
1060 symrev=webutil.symrevorshortnode(req, fctx),
1063 symrev=webutil.symrevorshortnode(req, fctx),
1061 entries=entries,
1064 entries=entries,
1062 descend=descend,
1065 descend=descend,
1063 patch=patch,
1066 patch=patch,
1064 latestentry=latestentry,
1067 latestentry=latestentry,
1065 linerange=linerange,
1068 linerange=linerange,
1066 revcount=revcount,
1069 revcount=revcount,
1067 morevars=morevars,
1070 morevars=morevars,
1068 lessvars=lessvars,
1071 lessvars=lessvars,
1069 **webutil.commonentry(web.repo, fctx))
1072 **webutil.commonentry(web.repo, fctx))
1070
1073
1071 @webcommand('archive')
1074 @webcommand('archive')
1072 def archive(web, req, tmpl):
1075 def archive(web, req, tmpl):
1073 """
1076 """
1074 /archive/{revision}.{format}[/{path}]
1077 /archive/{revision}.{format}[/{path}]
1075 -------------------------------------
1078 -------------------------------------
1076
1079
1077 Obtain an archive of repository content.
1080 Obtain an archive of repository content.
1078
1081
1079 The content and type of the archive is defined by a URL path parameter.
1082 The content and type of the archive is defined by a URL path parameter.
1080 ``format`` is the file extension of the archive type to be generated. e.g.
1083 ``format`` is the file extension of the archive type to be generated. e.g.
1081 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1084 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1082 server configuration.
1085 server configuration.
1083
1086
1084 The optional ``path`` URL parameter controls content to include in the
1087 The optional ``path`` URL parameter controls content to include in the
1085 archive. If omitted, every file in the specified revision is present in the
1088 archive. If omitted, every file in the specified revision is present in the
1086 archive. If included, only the specified file or contents of the specified
1089 archive. If included, only the specified file or contents of the specified
1087 directory will be included in the archive.
1090 directory will be included in the archive.
1088
1091
1089 No template is used for this handler. Raw, binary content is generated.
1092 No template is used for this handler. Raw, binary content is generated.
1090 """
1093 """
1091
1094
1092 type_ = req.form.get('type', [None])[0]
1095 type_ = req.form.get('type', [None])[0]
1093 allowed = web.configlist("web", "allow_archive")
1096 allowed = web.configlist("web", "allow_archive")
1094 key = req.form['node'][0]
1097 key = req.form['node'][0]
1095
1098
1096 if type_ not in web.archivespecs:
1099 if type_ not in web.archivespecs:
1097 msg = 'Unsupported archive type: %s' % type_
1100 msg = 'Unsupported archive type: %s' % type_
1098 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1101 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1099
1102
1100 if not ((type_ in allowed or
1103 if not ((type_ in allowed or
1101 web.configbool("web", "allow" + type_, False))):
1104 web.configbool("web", "allow" + type_, False))):
1102 msg = 'Archive type not allowed: %s' % type_
1105 msg = 'Archive type not allowed: %s' % type_
1103 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1106 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1104
1107
1105 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1108 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1106 cnode = web.repo.lookup(key)
1109 cnode = web.repo.lookup(key)
1107 arch_version = key
1110 arch_version = key
1108 if cnode == key or key == 'tip':
1111 if cnode == key or key == 'tip':
1109 arch_version = short(cnode)
1112 arch_version = short(cnode)
1110 name = "%s-%s" % (reponame, arch_version)
1113 name = "%s-%s" % (reponame, arch_version)
1111
1114
1112 ctx = webutil.changectx(web.repo, req)
1115 ctx = webutil.changectx(web.repo, req)
1113 pats = []
1116 pats = []
1114 match = scmutil.match(ctx, [])
1117 match = scmutil.match(ctx, [])
1115 file = req.form.get('file', None)
1118 file = req.form.get('file', None)
1116 if file:
1119 if file:
1117 pats = ['path:' + file[0]]
1120 pats = ['path:' + file[0]]
1118 match = scmutil.match(ctx, pats, default='path')
1121 match = scmutil.match(ctx, pats, default='path')
1119 if pats:
1122 if pats:
1120 files = [f for f in ctx.manifest().keys() if match(f)]
1123 files = [f for f in ctx.manifest().keys() if match(f)]
1121 if not files:
1124 if not files:
1122 raise ErrorResponse(HTTP_NOT_FOUND,
1125 raise ErrorResponse(HTTP_NOT_FOUND,
1123 'file(s) not found: %s' % file[0])
1126 'file(s) not found: %s' % file[0])
1124
1127
1125 mimetype, artype, extension, encoding = web.archivespecs[type_]
1128 mimetype, artype, extension, encoding = web.archivespecs[type_]
1126 headers = [
1129 headers = [
1127 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1130 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1128 ]
1131 ]
1129 if encoding:
1132 if encoding:
1130 headers.append(('Content-Encoding', encoding))
1133 headers.append(('Content-Encoding', encoding))
1131 req.headers.extend(headers)
1134 req.headers.extend(headers)
1132 req.respond(HTTP_OK, mimetype)
1135 req.respond(HTTP_OK, mimetype)
1133
1136
1134 archival.archive(web.repo, req, cnode, artype, prefix=name,
1137 archival.archive(web.repo, req, cnode, artype, prefix=name,
1135 matchfn=match,
1138 matchfn=match,
1136 subrepos=web.configbool("web", "archivesubrepos"))
1139 subrepos=web.configbool("web", "archivesubrepos"))
1137 return []
1140 return []
1138
1141
1139
1142
1140 @webcommand('static')
1143 @webcommand('static')
1141 def static(web, req, tmpl):
1144 def static(web, req, tmpl):
1142 fname = req.form['file'][0]
1145 fname = req.form['file'][0]
1143 # a repo owner may set web.static in .hg/hgrc to get any file
1146 # a repo owner may set web.static in .hg/hgrc to get any file
1144 # readable by the user running the CGI script
1147 # readable by the user running the CGI script
1145 static = web.config("web", "static", None, untrusted=False)
1148 static = web.config("web", "static", None, untrusted=False)
1146 if not static:
1149 if not static:
1147 tp = web.templatepath or templater.templatepaths()
1150 tp = web.templatepath or templater.templatepaths()
1148 if isinstance(tp, str):
1151 if isinstance(tp, str):
1149 tp = [tp]
1152 tp = [tp]
1150 static = [os.path.join(p, 'static') for p in tp]
1153 static = [os.path.join(p, 'static') for p in tp]
1151 staticfile(static, fname, req)
1154 staticfile(static, fname, req)
1152 return []
1155 return []
1153
1156
1154 @webcommand('graph')
1157 @webcommand('graph')
1155 def graph(web, req, tmpl):
1158 def graph(web, req, tmpl):
1156 """
1159 """
1157 /graph[/{revision}]
1160 /graph[/{revision}]
1158 -------------------
1161 -------------------
1159
1162
1160 Show information about the graphical topology of the repository.
1163 Show information about the graphical topology of the repository.
1161
1164
1162 Information rendered by this handler can be used to create visual
1165 Information rendered by this handler can be used to create visual
1163 representations of repository topology.
1166 representations of repository topology.
1164
1167
1165 The ``revision`` URL parameter controls the starting changeset.
1168 The ``revision`` URL parameter controls the starting changeset.
1166
1169
1167 The ``revcount`` query string argument can define the number of changesets
1170 The ``revcount`` query string argument can define the number of changesets
1168 to show information for.
1171 to show information for.
1169
1172
1170 This handler will render the ``graph`` template.
1173 This handler will render the ``graph`` template.
1171 """
1174 """
1172
1175
1173 if 'node' in req.form:
1176 if 'node' in req.form:
1174 ctx = webutil.changectx(web.repo, req)
1177 ctx = webutil.changectx(web.repo, req)
1175 symrev = webutil.symrevorshortnode(req, ctx)
1178 symrev = webutil.symrevorshortnode(req, ctx)
1176 else:
1179 else:
1177 ctx = web.repo['tip']
1180 ctx = web.repo['tip']
1178 symrev = 'tip'
1181 symrev = 'tip'
1179 rev = ctx.rev()
1182 rev = ctx.rev()
1180
1183
1181 bg_height = 39
1184 bg_height = 39
1182 revcount = web.maxshortchanges
1185 revcount = web.maxshortchanges
1183 if 'revcount' in req.form:
1186 if 'revcount' in req.form:
1184 try:
1187 try:
1185 revcount = int(req.form.get('revcount', [revcount])[0])
1188 revcount = int(req.form.get('revcount', [revcount])[0])
1186 revcount = max(revcount, 1)
1189 revcount = max(revcount, 1)
1187 tmpl.defaults['sessionvars']['revcount'] = revcount
1190 tmpl.defaults['sessionvars']['revcount'] = revcount
1188 except ValueError:
1191 except ValueError:
1189 pass
1192 pass
1190
1193
1191 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1194 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1192 lessvars['revcount'] = max(revcount / 2, 1)
1195 lessvars['revcount'] = max(revcount / 2, 1)
1193 morevars = copy.copy(tmpl.defaults['sessionvars'])
1196 morevars = copy.copy(tmpl.defaults['sessionvars'])
1194 morevars['revcount'] = revcount * 2
1197 morevars['revcount'] = revcount * 2
1195
1198
1196 count = len(web.repo)
1199 count = len(web.repo)
1197 pos = rev
1200 pos = rev
1198
1201
1199 uprev = min(max(0, count - 1), rev + revcount)
1202 uprev = min(max(0, count - 1), rev + revcount)
1200 downrev = max(0, rev - revcount)
1203 downrev = max(0, rev - revcount)
1201 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1204 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1202
1205
1203 tree = []
1206 tree = []
1204 if pos != -1:
1207 if pos != -1:
1205 allrevs = web.repo.changelog.revs(pos, 0)
1208 allrevs = web.repo.changelog.revs(pos, 0)
1206 revs = []
1209 revs = []
1207 for i in allrevs:
1210 for i in allrevs:
1208 revs.append(i)
1211 revs.append(i)
1209 if len(revs) >= revcount:
1212 if len(revs) >= revcount:
1210 break
1213 break
1211
1214
1212 # We have to feed a baseset to dagwalker as it is expecting smartset
1215 # We have to feed a baseset to dagwalker as it is expecting smartset
1213 # object. This does not have a big impact on hgweb performance itself
1216 # object. This does not have a big impact on hgweb performance itself
1214 # since hgweb graphing code is not itself lazy yet.
1217 # since hgweb graphing code is not itself lazy yet.
1215 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1218 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1216 # As we said one line above... not lazy.
1219 # As we said one line above... not lazy.
1217 tree = list(graphmod.colored(dag, web.repo))
1220 tree = list(graphmod.colored(dag, web.repo))
1218
1221
1219 def getcolumns(tree):
1222 def getcolumns(tree):
1220 cols = 0
1223 cols = 0
1221 for (id, type, ctx, vtx, edges) in tree:
1224 for (id, type, ctx, vtx, edges) in tree:
1222 if type != graphmod.CHANGESET:
1225 if type != graphmod.CHANGESET:
1223 continue
1226 continue
1224 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1227 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1225 max([edge[1] for edge in edges] or [0]))
1228 max([edge[1] for edge in edges] or [0]))
1226 return cols
1229 return cols
1227
1230
1228 def graphdata(usetuples, encodestr):
1231 def graphdata(usetuples, encodestr):
1229 data = []
1232 data = []
1230
1233
1231 row = 0
1234 row = 0
1232 for (id, type, ctx, vtx, edges) in tree:
1235 for (id, type, ctx, vtx, edges) in tree:
1233 if type != graphmod.CHANGESET:
1236 if type != graphmod.CHANGESET:
1234 continue
1237 continue
1235 node = str(ctx)
1238 node = str(ctx)
1236 age = encodestr(templatefilters.age(ctx.date()))
1239 age = encodestr(templatefilters.age(ctx.date()))
1237 desc = templatefilters.firstline(encodestr(ctx.description()))
1240 desc = templatefilters.firstline(encodestr(ctx.description()))
1238 desc = cgi.escape(templatefilters.nonempty(desc))
1241 desc = cgi.escape(templatefilters.nonempty(desc))
1239 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1242 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1240 branch = cgi.escape(encodestr(ctx.branch()))
1243 branch = cgi.escape(encodestr(ctx.branch()))
1241 try:
1244 try:
1242 branchnode = web.repo.branchtip(branch)
1245 branchnode = web.repo.branchtip(branch)
1243 except error.RepoLookupError:
1246 except error.RepoLookupError:
1244 branchnode = None
1247 branchnode = None
1245 branch = branch, branchnode == ctx.node()
1248 branch = branch, branchnode == ctx.node()
1246
1249
1247 if usetuples:
1250 if usetuples:
1248 data.append((node, vtx, edges, desc, user, age, branch,
1251 data.append((node, vtx, edges, desc, user, age, branch,
1249 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1252 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1250 [cgi.escape(encodestr(x))
1253 [cgi.escape(encodestr(x))
1251 for x in ctx.bookmarks()]))
1254 for x in ctx.bookmarks()]))
1252 else:
1255 else:
1253 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1256 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1254 'color': (edge[2] - 1) % 6 + 1,
1257 'color': (edge[2] - 1) % 6 + 1,
1255 'width': edge[3], 'bcolor': edge[4]}
1258 'width': edge[3], 'bcolor': edge[4]}
1256 for edge in edges]
1259 for edge in edges]
1257
1260
1258 data.append(
1261 data.append(
1259 {'node': node,
1262 {'node': node,
1260 'col': vtx[0],
1263 'col': vtx[0],
1261 'color': (vtx[1] - 1) % 6 + 1,
1264 'color': (vtx[1] - 1) % 6 + 1,
1262 'edges': edgedata,
1265 'edges': edgedata,
1263 'row': row,
1266 'row': row,
1264 'nextrow': row + 1,
1267 'nextrow': row + 1,
1265 'desc': desc,
1268 'desc': desc,
1266 'user': user,
1269 'user': user,
1267 'age': age,
1270 'age': age,
1268 'bookmarks': webutil.nodebookmarksdict(
1271 'bookmarks': webutil.nodebookmarksdict(
1269 web.repo, ctx.node()),
1272 web.repo, ctx.node()),
1270 'branches': webutil.nodebranchdict(web.repo, ctx),
1273 'branches': webutil.nodebranchdict(web.repo, ctx),
1271 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1274 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1272 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1275 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1273
1276
1274 row += 1
1277 row += 1
1275
1278
1276 return data
1279 return data
1277
1280
1278 cols = getcolumns(tree)
1281 cols = getcolumns(tree)
1279 rows = len(tree)
1282 rows = len(tree)
1280 canvasheight = (rows + 1) * bg_height - 27
1283 canvasheight = (rows + 1) * bg_height - 27
1281
1284
1282 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1285 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1283 uprev=uprev,
1286 uprev=uprev,
1284 lessvars=lessvars, morevars=morevars, downrev=downrev,
1287 lessvars=lessvars, morevars=morevars, downrev=downrev,
1285 cols=cols, rows=rows,
1288 cols=cols, rows=rows,
1286 canvaswidth=(cols + 1) * bg_height,
1289 canvaswidth=(cols + 1) * bg_height,
1287 truecanvasheight=rows * bg_height,
1290 truecanvasheight=rows * bg_height,
1288 canvasheight=canvasheight, bg_height=bg_height,
1291 canvasheight=canvasheight, bg_height=bg_height,
1289 # {jsdata} will be passed to |json, so it must be in utf-8
1292 # {jsdata} will be passed to |json, so it must be in utf-8
1290 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1293 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1291 nodes=lambda **x: graphdata(False, str),
1294 nodes=lambda **x: graphdata(False, str),
1292 node=ctx.hex(), changenav=changenav)
1295 node=ctx.hex(), changenav=changenav)
1293
1296
1294 def _getdoc(e):
1297 def _getdoc(e):
1295 doc = e[0].__doc__
1298 doc = e[0].__doc__
1296 if doc:
1299 if doc:
1297 doc = _(doc).partition('\n')[0]
1300 doc = _(doc).partition('\n')[0]
1298 else:
1301 else:
1299 doc = _('(no help text available)')
1302 doc = _('(no help text available)')
1300 return doc
1303 return doc
1301
1304
1302 @webcommand('help')
1305 @webcommand('help')
1303 def help(web, req, tmpl):
1306 def help(web, req, tmpl):
1304 """
1307 """
1305 /help[/{topic}]
1308 /help[/{topic}]
1306 ---------------
1309 ---------------
1307
1310
1308 Render help documentation.
1311 Render help documentation.
1309
1312
1310 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1313 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1311 is defined, that help topic will be rendered. If not, an index of
1314 is defined, that help topic will be rendered. If not, an index of
1312 available help topics will be rendered.
1315 available help topics will be rendered.
1313
1316
1314 The ``help`` template will be rendered when requesting help for a topic.
1317 The ``help`` template will be rendered when requesting help for a topic.
1315 ``helptopics`` will be rendered for the index of help topics.
1318 ``helptopics`` will be rendered for the index of help topics.
1316 """
1319 """
1317 from .. import commands, help as helpmod # avoid cycle
1320 from .. import commands, help as helpmod # avoid cycle
1318
1321
1319 topicname = req.form.get('node', [None])[0]
1322 topicname = req.form.get('node', [None])[0]
1320 if not topicname:
1323 if not topicname:
1321 def topics(**map):
1324 def topics(**map):
1322 for entries, summary, _doc in helpmod.helptable:
1325 for entries, summary, _doc in helpmod.helptable:
1323 yield {'topic': entries[0], 'summary': summary}
1326 yield {'topic': entries[0], 'summary': summary}
1324
1327
1325 early, other = [], []
1328 early, other = [], []
1326 primary = lambda s: s.partition('|')[0]
1329 primary = lambda s: s.partition('|')[0]
1327 for c, e in commands.table.iteritems():
1330 for c, e in commands.table.iteritems():
1328 doc = _getdoc(e)
1331 doc = _getdoc(e)
1329 if 'DEPRECATED' in doc or c.startswith('debug'):
1332 if 'DEPRECATED' in doc or c.startswith('debug'):
1330 continue
1333 continue
1331 cmd = primary(c)
1334 cmd = primary(c)
1332 if cmd.startswith('^'):
1335 if cmd.startswith('^'):
1333 early.append((cmd[1:], doc))
1336 early.append((cmd[1:], doc))
1334 else:
1337 else:
1335 other.append((cmd, doc))
1338 other.append((cmd, doc))
1336
1339
1337 early.sort()
1340 early.sort()
1338 other.sort()
1341 other.sort()
1339
1342
1340 def earlycommands(**map):
1343 def earlycommands(**map):
1341 for c, doc in early:
1344 for c, doc in early:
1342 yield {'topic': c, 'summary': doc}
1345 yield {'topic': c, 'summary': doc}
1343
1346
1344 def othercommands(**map):
1347 def othercommands(**map):
1345 for c, doc in other:
1348 for c, doc in other:
1346 yield {'topic': c, 'summary': doc}
1349 yield {'topic': c, 'summary': doc}
1347
1350
1348 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1351 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1349 othercommands=othercommands, title='Index')
1352 othercommands=othercommands, title='Index')
1350
1353
1351 # Render an index of sub-topics.
1354 # Render an index of sub-topics.
1352 if topicname in helpmod.subtopics:
1355 if topicname in helpmod.subtopics:
1353 topics = []
1356 topics = []
1354 for entries, summary, _doc in helpmod.subtopics[topicname]:
1357 for entries, summary, _doc in helpmod.subtopics[topicname]:
1355 topics.append({
1358 topics.append({
1356 'topic': '%s.%s' % (topicname, entries[0]),
1359 'topic': '%s.%s' % (topicname, entries[0]),
1357 'basename': entries[0],
1360 'basename': entries[0],
1358 'summary': summary,
1361 'summary': summary,
1359 })
1362 })
1360
1363
1361 return tmpl('helptopics', topics=topics, title=topicname,
1364 return tmpl('helptopics', topics=topics, title=topicname,
1362 subindex=True)
1365 subindex=True)
1363
1366
1364 u = webutil.wsgiui.load()
1367 u = webutil.wsgiui.load()
1365 u.verbose = True
1368 u.verbose = True
1366
1369
1367 # Render a page from a sub-topic.
1370 # Render a page from a sub-topic.
1368 if '.' in topicname:
1371 if '.' in topicname:
1369 # TODO implement support for rendering sections, like
1372 # TODO implement support for rendering sections, like
1370 # `hg help` works.
1373 # `hg help` works.
1371 topic, subtopic = topicname.split('.', 1)
1374 topic, subtopic = topicname.split('.', 1)
1372 if topic not in helpmod.subtopics:
1375 if topic not in helpmod.subtopics:
1373 raise ErrorResponse(HTTP_NOT_FOUND)
1376 raise ErrorResponse(HTTP_NOT_FOUND)
1374 else:
1377 else:
1375 topic = topicname
1378 topic = topicname
1376 subtopic = None
1379 subtopic = None
1377
1380
1378 try:
1381 try:
1379 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1382 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1380 except error.UnknownCommand:
1383 except error.UnknownCommand:
1381 raise ErrorResponse(HTTP_NOT_FOUND)
1384 raise ErrorResponse(HTTP_NOT_FOUND)
1382 return tmpl('help', topic=topicname, doc=doc)
1385 return tmpl('help', topic=topicname, doc=doc)
1383
1386
1384 # tell hggettext to extract docstrings from these functions:
1387 # tell hggettext to extract docstrings from these functions:
1385 i18nfunctions = commands.values()
1388 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now