##// END OF EJS Templates
hooks: adjust to handle protected branch cases, and force push.
marcink -
r509:1db1b667 default
parent child Browse files
Show More
@@ -1,106 +1,116 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2018 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """
18 """
19 Special exception handling over the wire.
19 Special exception handling over the wire.
20
20
21 Since we cannot assume that our client is able to import our exception classes,
21 Since we cannot assume that our client is able to import our exception classes,
22 this module provides a "wrapping" mechanism to raise plain exceptions
22 this module provides a "wrapping" mechanism to raise plain exceptions
23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
24 different error conditions.
24 different error conditions.
25 """
25 """
26
26
27 import functools
27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
28 from pyramid.httpexceptions import HTTPLocked
29
28
30
29
31 def _make_exception(kind, org_exc, *args):
30 def _make_exception(kind, org_exc, *args):
32 """
31 """
33 Prepares a base `Exception` instance to be sent over the wire.
32 Prepares a base `Exception` instance to be sent over the wire.
34
33
35 To give our caller a hint what this is about, it will attach an attribute
34 To give our caller a hint what this is about, it will attach an attribute
36 `_vcs_kind` to the exception.
35 `_vcs_kind` to the exception.
37 """
36 """
38 exc = Exception(*args)
37 exc = Exception(*args)
39 exc._vcs_kind = kind
38 exc._vcs_kind = kind
40 exc._org_exc = org_exc
39 exc._org_exc = org_exc
41 return exc
40 return exc
42
41
43
42
44 def AbortException(org_exc=None):
43 def AbortException(org_exc=None):
45 def _make_exception_wrapper(*args):
44 def _make_exception_wrapper(*args):
46 return _make_exception('abort', org_exc, *args)
45 return _make_exception('abort', org_exc, *args)
47 return _make_exception_wrapper
46 return _make_exception_wrapper
48
47
49
48
50 def ArchiveException(org_exc=None):
49 def ArchiveException(org_exc=None):
51 def _make_exception_wrapper(*args):
50 def _make_exception_wrapper(*args):
52 return _make_exception('archive', org_exc, *args)
51 return _make_exception('archive', org_exc, *args)
53 return _make_exception_wrapper
52 return _make_exception_wrapper
54
53
55
54
56 def LookupException(org_exc=None):
55 def LookupException(org_exc=None):
57 def _make_exception_wrapper(*args):
56 def _make_exception_wrapper(*args):
58 return _make_exception('lookup', org_exc, *args)
57 return _make_exception('lookup', org_exc, *args)
59 return _make_exception_wrapper
58 return _make_exception_wrapper
60
59
61
60
62 def VcsException(org_exc=None):
61 def VcsException(org_exc=None):
63 def _make_exception_wrapper(*args):
62 def _make_exception_wrapper(*args):
64 return _make_exception('error', org_exc, *args)
63 return _make_exception('error', org_exc, *args)
65 return _make_exception_wrapper
64 return _make_exception_wrapper
66
65
67
66
68 def RepositoryLockedException(org_exc=None):
67 def RepositoryLockedException(org_exc=None):
69 def _make_exception_wrapper(*args):
68 def _make_exception_wrapper(*args):
70 return _make_exception('repo_locked', org_exc, *args)
69 return _make_exception('repo_locked', org_exc, *args)
71 return _make_exception_wrapper
70 return _make_exception_wrapper
72
71
73
72
73 def RepositoryBranchProtectedException(org_exc=None):
74 def _make_exception_wrapper(*args):
75 return _make_exception('repo_branch_protected', org_exc, *args)
76 return _make_exception_wrapper
77
78
74 def RequirementException(org_exc=None):
79 def RequirementException(org_exc=None):
75 def _make_exception_wrapper(*args):
80 def _make_exception_wrapper(*args):
76 return _make_exception('requirement', org_exc, *args)
81 return _make_exception('requirement', org_exc, *args)
77 return _make_exception_wrapper
82 return _make_exception_wrapper
78
83
79
84
80 def UnhandledException(org_exc=None):
85 def UnhandledException(org_exc=None):
81 def _make_exception_wrapper(*args):
86 def _make_exception_wrapper(*args):
82 return _make_exception('unhandled', org_exc, *args)
87 return _make_exception('unhandled', org_exc, *args)
83 return _make_exception_wrapper
88 return _make_exception_wrapper
84
89
85
90
86 def URLError(org_exc=None):
91 def URLError(org_exc=None):
87 def _make_exception_wrapper(*args):
92 def _make_exception_wrapper(*args):
88 return _make_exception('url_error', org_exc, *args)
93 return _make_exception('url_error', org_exc, *args)
89 return _make_exception_wrapper
94 return _make_exception_wrapper
90
95
91
96
92 def SubrepoMergeException(org_exc=None):
97 def SubrepoMergeException(org_exc=None):
93 def _make_exception_wrapper(*args):
98 def _make_exception_wrapper(*args):
94 return _make_exception('subrepo_merge_error', org_exc, *args)
99 return _make_exception('subrepo_merge_error', org_exc, *args)
95 return _make_exception_wrapper
100 return _make_exception_wrapper
96
101
97
102
98 class HTTPRepoLocked(HTTPLocked):
103 class HTTPRepoLocked(HTTPLocked):
99 """
104 """
100 Subclass of HTTPLocked response that allows to set the title and status
105 Subclass of HTTPLocked response that allows to set the title and status
101 code via constructor arguments.
106 code via constructor arguments.
102 """
107 """
103 def __init__(self, title, status_code=None, **kwargs):
108 def __init__(self, title, status_code=None, **kwargs):
104 self.code = status_code or HTTPLocked.code
109 self.code = status_code or HTTPLocked.code
105 self.title = title
110 self.title = title
106 super(HTTPRepoLocked, self).__init__(**kwargs)
111 super(HTTPRepoLocked, self).__init__(**kwargs)
112
113
114 class HTTPRepoBranchProtected(HTTPForbidden):
115 def __init__(self, *args, **kwargs):
116 super(HTTPForbidden, self).__init__(*args, **kwargs)
@@ -1,572 +1,653 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2018 RhodeCode GmbH
5 #
5 #
6 # This program is free software; you can redistribute it and/or modify
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
9 # (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
20 import io
20 import io
21 import os
21 import os
22 import sys
22 import sys
23 import logging
23 import logging
24 import collections
24 import collections
25 import importlib
25 import importlib
26 import base64
26 import base64
27
27
28 from httplib import HTTPConnection
28 from httplib import HTTPConnection
29
29
30
30
31 import mercurial.scmutil
31 import mercurial.scmutil
32 import mercurial.node
32 import mercurial.node
33 import simplejson as json
33 import simplejson as json
34
34
35 from vcsserver import exceptions, subprocessio, settings
35 from vcsserver import exceptions, subprocessio, settings
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class HooksHttpClient(object):
40 class HooksHttpClient(object):
41 connection = None
41 connection = None
42
42
43 def __init__(self, hooks_uri):
43 def __init__(self, hooks_uri):
44 self.hooks_uri = hooks_uri
44 self.hooks_uri = hooks_uri
45
45
46 def __call__(self, method, extras):
46 def __call__(self, method, extras):
47 connection = HTTPConnection(self.hooks_uri)
47 connection = HTTPConnection(self.hooks_uri)
48 body = self._serialize(method, extras)
48 body = self._serialize(method, extras)
49 try:
49 try:
50 connection.request('POST', '/', body)
50 connection.request('POST', '/', body)
51 except Exception:
51 except Exception:
52 log.error('Connection failed on %s', connection)
52 log.error('Connection failed on %s', connection)
53 raise
53 raise
54 response = connection.getresponse()
54 response = connection.getresponse()
55 return json.loads(response.read())
55 return json.loads(response.read())
56
56
57 def _serialize(self, hook_name, extras):
57 def _serialize(self, hook_name, extras):
58 data = {
58 data = {
59 'method': hook_name,
59 'method': hook_name,
60 'extras': extras
60 'extras': extras
61 }
61 }
62 return json.dumps(data)
62 return json.dumps(data)
63
63
64
64
65 class HooksDummyClient(object):
65 class HooksDummyClient(object):
66 def __init__(self, hooks_module):
66 def __init__(self, hooks_module):
67 self._hooks_module = importlib.import_module(hooks_module)
67 self._hooks_module = importlib.import_module(hooks_module)
68
68
69 def __call__(self, hook_name, extras):
69 def __call__(self, hook_name, extras):
70 with self._hooks_module.Hooks() as hooks:
70 with self._hooks_module.Hooks() as hooks:
71 return getattr(hooks, hook_name)(extras)
71 return getattr(hooks, hook_name)(extras)
72
72
73
73
74 class RemoteMessageWriter(object):
74 class RemoteMessageWriter(object):
75 """Writer base class."""
75 """Writer base class."""
76 def write(self, message):
76 def write(self, message):
77 raise NotImplementedError()
77 raise NotImplementedError()
78
78
79
79
80 class HgMessageWriter(RemoteMessageWriter):
80 class HgMessageWriter(RemoteMessageWriter):
81 """Writer that knows how to send messages to mercurial clients."""
81 """Writer that knows how to send messages to mercurial clients."""
82
82
83 def __init__(self, ui):
83 def __init__(self, ui):
84 self.ui = ui
84 self.ui = ui
85
85
86 def write(self, message):
86 def write(self, message):
87 # TODO: Check why the quiet flag is set by default.
87 # TODO: Check why the quiet flag is set by default.
88 old = self.ui.quiet
88 old = self.ui.quiet
89 self.ui.quiet = False
89 self.ui.quiet = False
90 self.ui.status(message.encode('utf-8'))
90 self.ui.status(message.encode('utf-8'))
91 self.ui.quiet = old
91 self.ui.quiet = old
92
92
93
93
94 class GitMessageWriter(RemoteMessageWriter):
94 class GitMessageWriter(RemoteMessageWriter):
95 """Writer that knows how to send messages to git clients."""
95 """Writer that knows how to send messages to git clients."""
96
96
97 def __init__(self, stdout=None):
97 def __init__(self, stdout=None):
98 self.stdout = stdout or sys.stdout
98 self.stdout = stdout or sys.stdout
99
99
100 def write(self, message):
100 def write(self, message):
101 self.stdout.write(message.encode('utf-8'))
101 self.stdout.write(message.encode('utf-8'))
102
102
103
103
104 class SvnMessageWriter(RemoteMessageWriter):
104 class SvnMessageWriter(RemoteMessageWriter):
105 """Writer that knows how to send messages to svn clients."""
105 """Writer that knows how to send messages to svn clients."""
106
106
107 def __init__(self, stderr=None):
107 def __init__(self, stderr=None):
108 # SVN needs data sent to stderr for back-to-client messaging
108 # SVN needs data sent to stderr for back-to-client messaging
109 self.stderr = stderr or sys.stderr
109 self.stderr = stderr or sys.stderr
110
110
111 def write(self, message):
111 def write(self, message):
112 self.stderr.write(message.encode('utf-8'))
112 self.stderr.write(message.encode('utf-8'))
113
113
114
114
115 def _handle_exception(result):
115 def _handle_exception(result):
116 exception_class = result.get('exception')
116 exception_class = result.get('exception')
117 exception_traceback = result.get('exception_traceback')
117 exception_traceback = result.get('exception_traceback')
118
118
119 if exception_traceback:
119 if exception_traceback:
120 log.error('Got traceback from remote call:%s', exception_traceback)
120 log.error('Got traceback from remote call:%s', exception_traceback)
121
121
122 if exception_class == 'HTTPLockedRC':
122 if exception_class == 'HTTPLockedRC':
123 raise exceptions.RepositoryLockedException()(*result['exception_args'])
123 raise exceptions.RepositoryLockedException()(*result['exception_args'])
124 elif exception_class == 'HTTPBranchProtected':
125 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
124 elif exception_class == 'RepositoryError':
126 elif exception_class == 'RepositoryError':
125 raise exceptions.VcsException()(*result['exception_args'])
127 raise exceptions.VcsException()(*result['exception_args'])
126 elif exception_class:
128 elif exception_class:
127 raise Exception('Got remote exception "%s" with args "%s"' %
129 raise Exception('Got remote exception "%s" with args "%s"' %
128 (exception_class, result['exception_args']))
130 (exception_class, result['exception_args']))
129
131
130
132
131 def _get_hooks_client(extras):
133 def _get_hooks_client(extras):
132 if 'hooks_uri' in extras:
134 if 'hooks_uri' in extras:
133 protocol = extras.get('hooks_protocol')
135 protocol = extras.get('hooks_protocol')
134 return HooksHttpClient(extras['hooks_uri'])
136 return HooksHttpClient(extras['hooks_uri'])
135 else:
137 else:
136 return HooksDummyClient(extras['hooks_module'])
138 return HooksDummyClient(extras['hooks_module'])
137
139
138
140
139 def _call_hook(hook_name, extras, writer):
141 def _call_hook(hook_name, extras, writer):
140 hooks_client = _get_hooks_client(extras)
142 hooks_client = _get_hooks_client(extras)
141 log.debug('Hooks, using client:%s', hooks_client)
143 log.debug('Hooks, using client:%s', hooks_client)
142 result = hooks_client(hook_name, extras)
144 result = hooks_client(hook_name, extras)
143 log.debug('Hooks got result: %s', result)
145 log.debug('Hooks got result: %s', result)
144 writer.write(result['output'])
146 writer.write(result['output'])
145 _handle_exception(result)
147 _handle_exception(result)
146
148
147 return result['status']
149 return result['status']
148
150
149
151
150 def _extras_from_ui(ui):
152 def _extras_from_ui(ui):
151 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
153 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
152 if not hook_data:
154 if not hook_data:
153 # maybe it's inside environ ?
155 # maybe it's inside environ ?
154 env_hook_data = os.environ.get('RC_SCM_DATA')
156 env_hook_data = os.environ.get('RC_SCM_DATA')
155 if env_hook_data:
157 if env_hook_data:
156 hook_data = env_hook_data
158 hook_data = env_hook_data
157
159
158 extras = {}
160 extras = {}
159 if hook_data:
161 if hook_data:
160 extras = json.loads(hook_data)
162 extras = json.loads(hook_data)
161 return extras
163 return extras
162
164
163
165
164 def _rev_range_hash(repo, node):
166 def _rev_range_hash(repo, node, check_heads=False):
165
167
166 commits = []
168 commits = []
169 revs = []
167 start = repo[node].rev()
170 start = repo[node].rev()
168 for rev in xrange(start, len(repo)):
171 end = len(repo)
172 for rev in range(start, end):
173 revs.append(rev)
169 ctx = repo[rev]
174 ctx = repo[rev]
170 commit_id = mercurial.node.hex(ctx.node())
175 commit_id = mercurial.node.hex(ctx.node())
171 branch = ctx.branch()
176 branch = ctx.branch()
172 commits.append((commit_id, branch))
177 commits.append((commit_id, branch))
173
178
174 return commits
179 parent_heads = []
180 if check_heads:
181 parent_heads = _check_heads(repo, start, end, revs)
182 return commits, parent_heads
183
184
185 def _check_heads(repo, start, end, commits):
186 changelog = repo.changelog
187 parents = set()
188
189 for new_rev in commits:
190 for p in changelog.parentrevs(new_rev):
191 if p == mercurial.node.nullrev:
192 continue
193 if p < start:
194 parents.add(p)
195
196 for p in parents:
197 branch = repo[p].branch()
198 # The heads descending from that parent, on the same branch
199 parent_heads = set([p])
200 reachable = set([p])
201 for x in xrange(p + 1, end):
202 if repo[x].branch() != branch:
203 continue
204 for pp in changelog.parentrevs(x):
205 if pp in reachable:
206 reachable.add(x)
207 parent_heads.discard(pp)
208 parent_heads.add(x)
209 # More than one head? Suggest merging
210 if len(parent_heads) > 1:
211 return list(parent_heads)
212
213 return []
175
214
176
215
177 def repo_size(ui, repo, **kwargs):
216 def repo_size(ui, repo, **kwargs):
178 extras = _extras_from_ui(ui)
217 extras = _extras_from_ui(ui)
179 return _call_hook('repo_size', extras, HgMessageWriter(ui))
218 return _call_hook('repo_size', extras, HgMessageWriter(ui))
180
219
181
220
182 def pre_pull(ui, repo, **kwargs):
221 def pre_pull(ui, repo, **kwargs):
183 extras = _extras_from_ui(ui)
222 extras = _extras_from_ui(ui)
184 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
223 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
185
224
186
225
187 def pre_pull_ssh(ui, repo, **kwargs):
226 def pre_pull_ssh(ui, repo, **kwargs):
188 extras = _extras_from_ui(ui)
227 extras = _extras_from_ui(ui)
189 if extras and extras.get('SSH'):
228 if extras and extras.get('SSH'):
190 return pre_pull(ui, repo, **kwargs)
229 return pre_pull(ui, repo, **kwargs)
191 return 0
230 return 0
192
231
193
232
194 def post_pull(ui, repo, **kwargs):
233 def post_pull(ui, repo, **kwargs):
195 extras = _extras_from_ui(ui)
234 extras = _extras_from_ui(ui)
196 return _call_hook('post_pull', extras, HgMessageWriter(ui))
235 return _call_hook('post_pull', extras, HgMessageWriter(ui))
197
236
198
237
199 def post_pull_ssh(ui, repo, **kwargs):
238 def post_pull_ssh(ui, repo, **kwargs):
200 extras = _extras_from_ui(ui)
239 extras = _extras_from_ui(ui)
201 if extras and extras.get('SSH'):
240 if extras and extras.get('SSH'):
202 return post_pull(ui, repo, **kwargs)
241 return post_pull(ui, repo, **kwargs)
203 return 0
242 return 0
204
243
205
244
206 def pre_push(ui, repo, node=None, **kwargs):
245 def pre_push(ui, repo, node=None, **kwargs):
246 """
247 Mercurial pre_push hook
248 """
207 extras = _extras_from_ui(ui)
249 extras = _extras_from_ui(ui)
250 detect_force_push = extras.get('detect_force_push')
208
251
209 rev_data = []
252 rev_data = []
210 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
253 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
211 branches = collections.defaultdict(list)
254 branches = collections.defaultdict(list)
212 for commit_id, branch in _rev_range_hash(repo, node):
255 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
256 for commit_id, branch in commits:
213 branches[branch].append(commit_id)
257 branches[branch].append(commit_id)
214
258
215 for branch, commits in branches.iteritems():
259 for branch, commits in branches.items():
216 old_rev = kwargs.get('node_last') or commits[0]
260 old_rev = kwargs.get('node_last') or commits[0]
217 rev_data.append({
261 rev_data.append({
218 'old_rev': old_rev,
262 'old_rev': old_rev,
219 'new_rev': commits[-1],
263 'new_rev': commits[-1],
220 'ref': '',
264 'ref': '',
221 'type': 'branch',
265 'type': 'branch',
222 'name': branch,
266 'name': branch,
223 })
267 })
224
268
269 for push_ref in rev_data:
270 push_ref['multiple_heads'] = _heads
271
225 extras['commit_ids'] = rev_data
272 extras['commit_ids'] = rev_data
226 return _call_hook('pre_push', extras, HgMessageWriter(ui))
273 return _call_hook('pre_push', extras, HgMessageWriter(ui))
227
274
228
275
229 def pre_push_ssh(ui, repo, node=None, **kwargs):
276 def pre_push_ssh(ui, repo, node=None, **kwargs):
230 if _extras_from_ui(ui).get('SSH'):
277 if _extras_from_ui(ui).get('SSH'):
231 return pre_push(ui, repo, node, **kwargs)
278 return pre_push(ui, repo, node, **kwargs)
232
279
233 return 0
280 return 0
234
281
235
282
236 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
283 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
284 """
285 Mercurial pre_push hook for SSH
286 """
237 extras = _extras_from_ui(ui)
287 extras = _extras_from_ui(ui)
238 if extras.get('SSH'):
288 if extras.get('SSH'):
239 permission = extras['SSH_PERMISSIONS']
289 permission = extras['SSH_PERMISSIONS']
240
290
241 if 'repository.write' == permission or 'repository.admin' == permission:
291 if 'repository.write' == permission or 'repository.admin' == permission:
242 return 0
292 return 0
243
293
244 # non-zero ret code
294 # non-zero ret code
245 return 1
295 return 1
246
296
247 return 0
297 return 0
248
298
249
299
250 def post_push(ui, repo, node, **kwargs):
300 def post_push(ui, repo, node, **kwargs):
301 """
302 Mercurial post_push hook
303 """
251 extras = _extras_from_ui(ui)
304 extras = _extras_from_ui(ui)
252
305
253 commit_ids = []
306 commit_ids = []
254 branches = []
307 branches = []
255 bookmarks = []
308 bookmarks = []
256 tags = []
309 tags = []
257
310
258 for commit_id, branch in _rev_range_hash(repo, node):
311 commits, _heads = _rev_range_hash(repo, node)
312 for commit_id, branch in commits:
259 commit_ids.append(commit_id)
313 commit_ids.append(commit_id)
260 if branch not in branches:
314 if branch not in branches:
261 branches.append(branch)
315 branches.append(branch)
262
316
263 if hasattr(ui, '_rc_pushkey_branches'):
317 if hasattr(ui, '_rc_pushkey_branches'):
264 bookmarks = ui._rc_pushkey_branches
318 bookmarks = ui._rc_pushkey_branches
265
319
266 extras['commit_ids'] = commit_ids
320 extras['commit_ids'] = commit_ids
267 extras['new_refs'] = {
321 extras['new_refs'] = {
268 'branches': branches,
322 'branches': branches,
269 'bookmarks': bookmarks,
323 'bookmarks': bookmarks,
270 'tags': tags
324 'tags': tags
271 }
325 }
272
326
273 return _call_hook('post_push', extras, HgMessageWriter(ui))
327 return _call_hook('post_push', extras, HgMessageWriter(ui))
274
328
275
329
276 def post_push_ssh(ui, repo, node, **kwargs):
330 def post_push_ssh(ui, repo, node, **kwargs):
331 """
332 Mercurial post_push hook for SSH
333 """
277 if _extras_from_ui(ui).get('SSH'):
334 if _extras_from_ui(ui).get('SSH'):
278 return post_push(ui, repo, node, **kwargs)
335 return post_push(ui, repo, node, **kwargs)
279 return 0
336 return 0
280
337
281
338
282 def key_push(ui, repo, **kwargs):
339 def key_push(ui, repo, **kwargs):
283 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
340 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
284 # store new bookmarks in our UI object propagated later to post_push
341 # store new bookmarks in our UI object propagated later to post_push
285 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
342 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
286 return
343 return
287
344
288
345
289 # backward compat
346 # backward compat
290 log_pull_action = post_pull
347 log_pull_action = post_pull
291
348
292 # backward compat
349 # backward compat
293 log_push_action = post_push
350 log_push_action = post_push
294
351
295
352
296 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
353 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
297 """
354 """
298 Old hook name: keep here for backward compatibility.
355 Old hook name: keep here for backward compatibility.
299
356
300 This is only required when the installed git hooks are not upgraded.
357 This is only required when the installed git hooks are not upgraded.
301 """
358 """
302 pass
359 pass
303
360
304
361
305 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
362 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
306 """
363 """
307 Old hook name: keep here for backward compatibility.
364 Old hook name: keep here for backward compatibility.
308
365
309 This is only required when the installed git hooks are not upgraded.
366 This is only required when the installed git hooks are not upgraded.
310 """
367 """
311 pass
368 pass
312
369
313
370
314 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
371 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
315
372
316
373
317 def git_pre_pull(extras):
374 def git_pre_pull(extras):
318 """
375 """
319 Pre pull hook.
376 Pre pull hook.
320
377
321 :param extras: dictionary containing the keys defined in simplevcs
378 :param extras: dictionary containing the keys defined in simplevcs
322 :type extras: dict
379 :type extras: dict
323
380
324 :return: status code of the hook. 0 for success.
381 :return: status code of the hook. 0 for success.
325 :rtype: int
382 :rtype: int
326 """
383 """
327 if 'pull' not in extras['hooks']:
384 if 'pull' not in extras['hooks']:
328 return HookResponse(0, '')
385 return HookResponse(0, '')
329
386
330 stdout = io.BytesIO()
387 stdout = io.BytesIO()
331 try:
388 try:
332 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
389 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
333 except Exception as error:
390 except Exception as error:
334 status = 128
391 status = 128
335 stdout.write('ERROR: %s\n' % str(error))
392 stdout.write('ERROR: %s\n' % str(error))
336
393
337 return HookResponse(status, stdout.getvalue())
394 return HookResponse(status, stdout.getvalue())
338
395
339
396
340 def git_post_pull(extras):
397 def git_post_pull(extras):
341 """
398 """
342 Post pull hook.
399 Post pull hook.
343
400
344 :param extras: dictionary containing the keys defined in simplevcs
401 :param extras: dictionary containing the keys defined in simplevcs
345 :type extras: dict
402 :type extras: dict
346
403
347 :return: status code of the hook. 0 for success.
404 :return: status code of the hook. 0 for success.
348 :rtype: int
405 :rtype: int
349 """
406 """
350 if 'pull' not in extras['hooks']:
407 if 'pull' not in extras['hooks']:
351 return HookResponse(0, '')
408 return HookResponse(0, '')
352
409
353 stdout = io.BytesIO()
410 stdout = io.BytesIO()
354 try:
411 try:
355 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
412 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
356 except Exception as error:
413 except Exception as error:
357 status = 128
414 status = 128
358 stdout.write('ERROR: %s\n' % error)
415 stdout.write('ERROR: %s\n' % error)
359
416
360 return HookResponse(status, stdout.getvalue())
417 return HookResponse(status, stdout.getvalue())
361
418
362
419
363 def _parse_git_ref_lines(revision_lines):
420 def _parse_git_ref_lines(revision_lines):
364 rev_data = []
421 rev_data = []
365 for revision_line in revision_lines or []:
422 for revision_line in revision_lines or []:
366 old_rev, new_rev, ref = revision_line.strip().split(' ')
423 old_rev, new_rev, ref = revision_line.strip().split(' ')
367 ref_data = ref.split('/', 2)
424 ref_data = ref.split('/', 2)
368 if ref_data[1] in ('tags', 'heads'):
425 if ref_data[1] in ('tags', 'heads'):
369 rev_data.append({
426 rev_data.append({
370 'old_rev': old_rev,
427 'old_rev': old_rev,
371 'new_rev': new_rev,
428 'new_rev': new_rev,
372 'ref': ref,
429 'ref': ref,
373 'type': ref_data[1],
430 'type': ref_data[1],
374 'name': ref_data[2],
431 'name': ref_data[2],
375 })
432 })
376 return rev_data
433 return rev_data
377
434
378
435
379 def git_pre_receive(unused_repo_path, revision_lines, env):
436 def git_pre_receive(unused_repo_path, revision_lines, env):
380 """
437 """
381 Pre push hook.
438 Pre push hook.
382
439
383 :param extras: dictionary containing the keys defined in simplevcs
440 :param extras: dictionary containing the keys defined in simplevcs
384 :type extras: dict
441 :type extras: dict
385
442
386 :return: status code of the hook. 0 for success.
443 :return: status code of the hook. 0 for success.
387 :rtype: int
444 :rtype: int
388 """
445 """
389 extras = json.loads(env['RC_SCM_DATA'])
446 extras = json.loads(env['RC_SCM_DATA'])
390 rev_data = _parse_git_ref_lines(revision_lines)
447 rev_data = _parse_git_ref_lines(revision_lines)
391 if 'push' not in extras['hooks']:
448 if 'push' not in extras['hooks']:
392 return 0
449 return 0
450 empty_commit_id = '0' * 40
451
452 detect_force_push = extras.get('detect_force_push')
453
454 for push_ref in rev_data:
455 push_ref['pruned_sha'] = ''
456 if not detect_force_push:
457 # don't check for forced-push when we don't need to
458 continue
459
460 type_ = push_ref['type']
461 new_branch = push_ref['old_rev'] == empty_commit_id
462 if type_ == 'heads' and not new_branch:
463 old_rev = push_ref['old_rev']
464 new_rev = push_ref['new_rev']
465 cmd = [settings.GIT_EXECUTABLE, 'rev-list',
466 old_rev, '^{}'.format(new_rev)]
467 stdout, stderr = subprocessio.run_command(
468 cmd, env=os.environ.copy())
469 # means we're having some non-reachable objects, this forced push
470 # was used
471 if stdout:
472 push_ref['pruned_sha'] = stdout.splitlines()
473
393 extras['commit_ids'] = rev_data
474 extras['commit_ids'] = rev_data
394 return _call_hook('pre_push', extras, GitMessageWriter())
475 return _call_hook('pre_push', extras, GitMessageWriter())
395
476
396
477
397 def git_post_receive(unused_repo_path, revision_lines, env):
478 def git_post_receive(unused_repo_path, revision_lines, env):
398 """
479 """
399 Post push hook.
480 Post push hook.
400
481
401 :param extras: dictionary containing the keys defined in simplevcs
482 :param extras: dictionary containing the keys defined in simplevcs
402 :type extras: dict
483 :type extras: dict
403
484
404 :return: status code of the hook. 0 for success.
485 :return: status code of the hook. 0 for success.
405 :rtype: int
486 :rtype: int
406 """
487 """
407 extras = json.loads(env['RC_SCM_DATA'])
488 extras = json.loads(env['RC_SCM_DATA'])
408 if 'push' not in extras['hooks']:
489 if 'push' not in extras['hooks']:
409 return 0
490 return 0
410
491
411 rev_data = _parse_git_ref_lines(revision_lines)
492 rev_data = _parse_git_ref_lines(revision_lines)
412
493
413 git_revs = []
494 git_revs = []
414
495
415 # N.B.(skreft): it is ok to just call git, as git before calling a
496 # N.B.(skreft): it is ok to just call git, as git before calling a
416 # subcommand sets the PATH environment variable so that it point to the
497 # subcommand sets the PATH environment variable so that it point to the
417 # correct version of the git executable.
498 # correct version of the git executable.
418 empty_commit_id = '0' * 40
499 empty_commit_id = '0' * 40
419 branches = []
500 branches = []
420 tags = []
501 tags = []
421 for push_ref in rev_data:
502 for push_ref in rev_data:
422 type_ = push_ref['type']
503 type_ = push_ref['type']
423
504
424 if type_ == 'heads':
505 if type_ == 'heads':
425 if push_ref['old_rev'] == empty_commit_id:
506 if push_ref['old_rev'] == empty_commit_id:
426 # starting new branch case
507 # starting new branch case
427 if push_ref['name'] not in branches:
508 if push_ref['name'] not in branches:
428 branches.append(push_ref['name'])
509 branches.append(push_ref['name'])
429
510
430 # Fix up head revision if needed
511 # Fix up head revision if needed
431 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
512 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
432 try:
513 try:
433 subprocessio.run_command(cmd, env=os.environ.copy())
514 subprocessio.run_command(cmd, env=os.environ.copy())
434 except Exception:
515 except Exception:
435 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
516 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
436 'refs/heads/%s' % push_ref['name']]
517 'refs/heads/%s' % push_ref['name']]
437 print("Setting default branch to %s" % push_ref['name'])
518 print("Setting default branch to %s" % push_ref['name'])
438 subprocessio.run_command(cmd, env=os.environ.copy())
519 subprocessio.run_command(cmd, env=os.environ.copy())
439
520
440 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
521 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
441 '--format=%(refname)', 'refs/heads/*']
522 '--format=%(refname)', 'refs/heads/*']
442 stdout, stderr = subprocessio.run_command(
523 stdout, stderr = subprocessio.run_command(
443 cmd, env=os.environ.copy())
524 cmd, env=os.environ.copy())
444 heads = stdout
525 heads = stdout
445 heads = heads.replace(push_ref['ref'], '')
526 heads = heads.replace(push_ref['ref'], '')
446 heads = ' '.join(head for head
527 heads = ' '.join(head for head
447 in heads.splitlines() if head) or '.'
528 in heads.splitlines() if head) or '.'
448 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
529 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
449 '--pretty=format:%H', '--', push_ref['new_rev'],
530 '--pretty=format:%H', '--', push_ref['new_rev'],
450 '--not', heads]
531 '--not', heads]
451 stdout, stderr = subprocessio.run_command(
532 stdout, stderr = subprocessio.run_command(
452 cmd, env=os.environ.copy())
533 cmd, env=os.environ.copy())
453 git_revs.extend(stdout.splitlines())
534 git_revs.extend(stdout.splitlines())
454 elif push_ref['new_rev'] == empty_commit_id:
535 elif push_ref['new_rev'] == empty_commit_id:
455 # delete branch case
536 # delete branch case
456 git_revs.append('delete_branch=>%s' % push_ref['name'])
537 git_revs.append('delete_branch=>%s' % push_ref['name'])
457 else:
538 else:
458 if push_ref['name'] not in branches:
539 if push_ref['name'] not in branches:
459 branches.append(push_ref['name'])
540 branches.append(push_ref['name'])
460
541
461 cmd = [settings.GIT_EXECUTABLE, 'log',
542 cmd = [settings.GIT_EXECUTABLE, 'log',
462 '{old_rev}..{new_rev}'.format(**push_ref),
543 '{old_rev}..{new_rev}'.format(**push_ref),
463 '--reverse', '--pretty=format:%H']
544 '--reverse', '--pretty=format:%H']
464 stdout, stderr = subprocessio.run_command(
545 stdout, stderr = subprocessio.run_command(
465 cmd, env=os.environ.copy())
546 cmd, env=os.environ.copy())
466 git_revs.extend(stdout.splitlines())
547 git_revs.extend(stdout.splitlines())
467 elif type_ == 'tags':
548 elif type_ == 'tags':
468 if push_ref['name'] not in tags:
549 if push_ref['name'] not in tags:
469 tags.append(push_ref['name'])
550 tags.append(push_ref['name'])
470 git_revs.append('tag=>%s' % push_ref['name'])
551 git_revs.append('tag=>%s' % push_ref['name'])
471
552
472 extras['commit_ids'] = git_revs
553 extras['commit_ids'] = git_revs
473 extras['new_refs'] = {
554 extras['new_refs'] = {
474 'branches': branches,
555 'branches': branches,
475 'bookmarks': [],
556 'bookmarks': [],
476 'tags': tags,
557 'tags': tags,
477 }
558 }
478
559
479 if 'repo_size' in extras['hooks']:
560 if 'repo_size' in extras['hooks']:
480 try:
561 try:
481 _call_hook('repo_size', extras, GitMessageWriter())
562 _call_hook('repo_size', extras, GitMessageWriter())
482 except:
563 except:
483 pass
564 pass
484
565
485 return _call_hook('post_push', extras, GitMessageWriter())
566 return _call_hook('post_push', extras, GitMessageWriter())
486
567
487
568
488 def _get_extras_from_txn_id(path, txn_id):
569 def _get_extras_from_txn_id(path, txn_id):
489 extras = {}
570 extras = {}
490 try:
571 try:
491 cmd = ['svnlook', 'pget',
572 cmd = ['svnlook', 'pget',
492 '-t', txn_id,
573 '-t', txn_id,
493 '--revprop', path, 'rc-scm-extras']
574 '--revprop', path, 'rc-scm-extras']
494 stdout, stderr = subprocessio.run_command(
575 stdout, stderr = subprocessio.run_command(
495 cmd, env=os.environ.copy())
576 cmd, env=os.environ.copy())
496 extras = json.loads(base64.urlsafe_b64decode(stdout))
577 extras = json.loads(base64.urlsafe_b64decode(stdout))
497 except Exception:
578 except Exception:
498 log.exception('Failed to extract extras info from txn_id')
579 log.exception('Failed to extract extras info from txn_id')
499
580
500 return extras
581 return extras
501
582
502
583
503 def svn_pre_commit(repo_path, commit_data, env):
584 def svn_pre_commit(repo_path, commit_data, env):
504 path, txn_id = commit_data
585 path, txn_id = commit_data
505 branches = []
586 branches = []
506 tags = []
587 tags = []
507
588
508 if env.get('RC_SCM_DATA'):
589 if env.get('RC_SCM_DATA'):
509 extras = json.loads(env['RC_SCM_DATA'])
590 extras = json.loads(env['RC_SCM_DATA'])
510 else:
591 else:
511 # fallback method to read from TXN-ID stored data
592 # fallback method to read from TXN-ID stored data
512 extras = _get_extras_from_txn_id(path, txn_id)
593 extras = _get_extras_from_txn_id(path, txn_id)
513 if not extras:
594 if not extras:
514 return 0
595 return 0
515
596
516 extras['commit_ids'] = []
597 extras['commit_ids'] = []
517 extras['txn_id'] = txn_id
598 extras['txn_id'] = txn_id
518 extras['new_refs'] = {
599 extras['new_refs'] = {
519 'branches': branches,
600 'branches': branches,
520 'bookmarks': [],
601 'bookmarks': [],
521 'tags': tags,
602 'tags': tags,
522 }
603 }
523
604
524 return _call_hook('pre_push', extras, SvnMessageWriter())
605 return _call_hook('pre_push', extras, SvnMessageWriter())
525
606
526
607
527 def _get_extras_from_commit_id(commit_id, path):
608 def _get_extras_from_commit_id(commit_id, path):
528 extras = {}
609 extras = {}
529 try:
610 try:
530 cmd = ['svnlook', 'pget',
611 cmd = ['svnlook', 'pget',
531 '-r', commit_id,
612 '-r', commit_id,
532 '--revprop', path, 'rc-scm-extras']
613 '--revprop', path, 'rc-scm-extras']
533 stdout, stderr = subprocessio.run_command(
614 stdout, stderr = subprocessio.run_command(
534 cmd, env=os.environ.copy())
615 cmd, env=os.environ.copy())
535 extras = json.loads(base64.urlsafe_b64decode(stdout))
616 extras = json.loads(base64.urlsafe_b64decode(stdout))
536 except Exception:
617 except Exception:
537 log.exception('Failed to extract extras info from commit_id')
618 log.exception('Failed to extract extras info from commit_id')
538
619
539 return extras
620 return extras
540
621
541
622
542 def svn_post_commit(repo_path, commit_data, env):
623 def svn_post_commit(repo_path, commit_data, env):
543 """
624 """
544 commit_data is path, rev, txn_id
625 commit_data is path, rev, txn_id
545 """
626 """
546 path, commit_id, txn_id = commit_data
627 path, commit_id, txn_id = commit_data
547 branches = []
628 branches = []
548 tags = []
629 tags = []
549
630
550 if env.get('RC_SCM_DATA'):
631 if env.get('RC_SCM_DATA'):
551 extras = json.loads(env['RC_SCM_DATA'])
632 extras = json.loads(env['RC_SCM_DATA'])
552 else:
633 else:
553 # fallback method to read from TXN-ID stored data
634 # fallback method to read from TXN-ID stored data
554 extras = _get_extras_from_commit_id(commit_id, path)
635 extras = _get_extras_from_commit_id(commit_id, path)
555 if not extras:
636 if not extras:
556 return 0
637 return 0
557
638
558 extras['commit_ids'] = [commit_id]
639 extras['commit_ids'] = [commit_id]
559 extras['txn_id'] = txn_id
640 extras['txn_id'] = txn_id
560 extras['new_refs'] = {
641 extras['new_refs'] = {
561 'branches': branches,
642 'branches': branches,
562 'bookmarks': [],
643 'bookmarks': [],
563 'tags': tags,
644 'tags': tags,
564 }
645 }
565
646
566 if 'repo_size' in extras['hooks']:
647 if 'repo_size' in extras['hooks']:
567 try:
648 try:
568 _call_hook('repo_size', extras, SvnMessageWriter())
649 _call_hook('repo_size', extras, SvnMessageWriter())
569 except Exception:
650 except Exception:
570 pass
651 pass
571
652
572 return _call_hook('post_push', extras, SvnMessageWriter())
653 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,559 +1,563 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2018 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import sys
19 import sys
20 import base64
20 import base64
21 import locale
21 import locale
22 import logging
22 import logging
23 import uuid
23 import uuid
24 import wsgiref.util
24 import wsgiref.util
25 import traceback
25 import traceback
26 from itertools import chain
26 from itertools import chain
27
27
28 import simplejson as json
28 import simplejson as json
29 import msgpack
29 import msgpack
30 from pyramid.config import Configurator
30 from pyramid.config import Configurator
31 from pyramid.settings import asbool, aslist
31 from pyramid.settings import asbool, aslist
32 from pyramid.wsgi import wsgiapp
32 from pyramid.wsgi import wsgiapp
33 from pyramid.compat import configparser
33 from pyramid.compat import configparser
34
34
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
38 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
39 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
39 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
40
40
41 try:
41 try:
42 locale.setlocale(locale.LC_ALL, '')
42 locale.setlocale(locale.LC_ALL, '')
43 except locale.Error as e:
43 except locale.Error as e:
44 log.error(
44 log.error(
45 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
45 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
46 os.environ['LC_ALL'] = 'C'
46 os.environ['LC_ALL'] = 'C'
47
47
48
48
49 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
49 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
50 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
50 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
51 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
51 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
52 from vcsserver.echo_stub.echo_app import EchoApp
52 from vcsserver.echo_stub.echo_app import EchoApp
53 from vcsserver.exceptions import HTTPRepoLocked
53 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
54 from vcsserver.lib.exc_tracking import store_exception
54 from vcsserver.lib.exc_tracking import store_exception
55 from vcsserver.server import VcsServer
55 from vcsserver.server import VcsServer
56
56
57 try:
57 try:
58 from vcsserver.git import GitFactory, GitRemote
58 from vcsserver.git import GitFactory, GitRemote
59 except ImportError:
59 except ImportError:
60 GitFactory = None
60 GitFactory = None
61 GitRemote = None
61 GitRemote = None
62
62
63 try:
63 try:
64 from vcsserver.hg import MercurialFactory, HgRemote
64 from vcsserver.hg import MercurialFactory, HgRemote
65 except ImportError:
65 except ImportError:
66 MercurialFactory = None
66 MercurialFactory = None
67 HgRemote = None
67 HgRemote = None
68
68
69 try:
69 try:
70 from vcsserver.svn import SubversionFactory, SvnRemote
70 from vcsserver.svn import SubversionFactory, SvnRemote
71 except ImportError:
71 except ImportError:
72 SubversionFactory = None
72 SubversionFactory = None
73 SvnRemote = None
73 SvnRemote = None
74
74
75
75
76
76
77
77
78 def _is_request_chunked(environ):
78 def _is_request_chunked(environ):
79 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
79 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
80 return stream
80 return stream
81
81
82
82
83 def _int_setting(settings, name, default):
83 def _int_setting(settings, name, default):
84 settings[name] = int(settings.get(name, default))
84 settings[name] = int(settings.get(name, default))
85
85
86
86
87 def _bool_setting(settings, name, default):
87 def _bool_setting(settings, name, default):
88 input_val = settings.get(name, default)
88 input_val = settings.get(name, default)
89 if isinstance(input_val, unicode):
89 if isinstance(input_val, unicode):
90 input_val = input_val.encode('utf8')
90 input_val = input_val.encode('utf8')
91 settings[name] = asbool(input_val)
91 settings[name] = asbool(input_val)
92
92
93
93
94 def _list_setting(settings, name, default):
94 def _list_setting(settings, name, default):
95 raw_value = settings.get(name, default)
95 raw_value = settings.get(name, default)
96
96
97 # Otherwise we assume it uses pyramids space/newline separation.
97 # Otherwise we assume it uses pyramids space/newline separation.
98 settings[name] = aslist(raw_value)
98 settings[name] = aslist(raw_value)
99
99
100
100
101 def _string_setting(settings, name, default, lower=True):
101 def _string_setting(settings, name, default, lower=True):
102 value = settings.get(name, default)
102 value = settings.get(name, default)
103 if lower:
103 if lower:
104 value = value.lower()
104 value = value.lower()
105 settings[name] = value
105 settings[name] = value
106
106
107
107
108 class VCS(object):
108 class VCS(object):
109 def __init__(self, locale=None, cache_config=None):
109 def __init__(self, locale=None, cache_config=None):
110 self.locale = locale
110 self.locale = locale
111 self.cache_config = cache_config
111 self.cache_config = cache_config
112 self._configure_locale()
112 self._configure_locale()
113
113
114 if GitFactory and GitRemote:
114 if GitFactory and GitRemote:
115 git_factory = GitFactory()
115 git_factory = GitFactory()
116 self._git_remote = GitRemote(git_factory)
116 self._git_remote = GitRemote(git_factory)
117 else:
117 else:
118 log.info("Git client import failed")
118 log.info("Git client import failed")
119
119
120 if MercurialFactory and HgRemote:
120 if MercurialFactory and HgRemote:
121 hg_factory = MercurialFactory()
121 hg_factory = MercurialFactory()
122 self._hg_remote = HgRemote(hg_factory)
122 self._hg_remote = HgRemote(hg_factory)
123 else:
123 else:
124 log.info("Mercurial client import failed")
124 log.info("Mercurial client import failed")
125
125
126 if SubversionFactory and SvnRemote:
126 if SubversionFactory and SvnRemote:
127 svn_factory = SubversionFactory()
127 svn_factory = SubversionFactory()
128
128
129 # hg factory is used for svn url validation
129 # hg factory is used for svn url validation
130 hg_factory = MercurialFactory()
130 hg_factory = MercurialFactory()
131 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
131 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
132 else:
132 else:
133 log.info("Subversion client import failed")
133 log.info("Subversion client import failed")
134
134
135 self._vcsserver = VcsServer()
135 self._vcsserver = VcsServer()
136
136
137 def _configure_locale(self):
137 def _configure_locale(self):
138 if self.locale:
138 if self.locale:
139 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
139 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
140 else:
140 else:
141 log.info(
141 log.info(
142 'Configuring locale subsystem based on environment variables')
142 'Configuring locale subsystem based on environment variables')
143 try:
143 try:
144 # If self.locale is the empty string, then the locale
144 # If self.locale is the empty string, then the locale
145 # module will use the environment variables. See the
145 # module will use the environment variables. See the
146 # documentation of the package `locale`.
146 # documentation of the package `locale`.
147 locale.setlocale(locale.LC_ALL, self.locale)
147 locale.setlocale(locale.LC_ALL, self.locale)
148
148
149 language_code, encoding = locale.getlocale()
149 language_code, encoding = locale.getlocale()
150 log.info(
150 log.info(
151 'Locale set to language code "%s" with encoding "%s".',
151 'Locale set to language code "%s" with encoding "%s".',
152 language_code, encoding)
152 language_code, encoding)
153 except locale.Error:
153 except locale.Error:
154 log.exception(
154 log.exception(
155 'Cannot set locale, not configuring the locale system')
155 'Cannot set locale, not configuring the locale system')
156
156
157
157
158 class WsgiProxy(object):
158 class WsgiProxy(object):
159 def __init__(self, wsgi):
159 def __init__(self, wsgi):
160 self.wsgi = wsgi
160 self.wsgi = wsgi
161
161
162 def __call__(self, environ, start_response):
162 def __call__(self, environ, start_response):
163 input_data = environ['wsgi.input'].read()
163 input_data = environ['wsgi.input'].read()
164 input_data = msgpack.unpackb(input_data)
164 input_data = msgpack.unpackb(input_data)
165
165
166 error = None
166 error = None
167 try:
167 try:
168 data, status, headers = self.wsgi.handle(
168 data, status, headers = self.wsgi.handle(
169 input_data['environment'], input_data['input_data'],
169 input_data['environment'], input_data['input_data'],
170 *input_data['args'], **input_data['kwargs'])
170 *input_data['args'], **input_data['kwargs'])
171 except Exception as e:
171 except Exception as e:
172 data, status, headers = [], None, None
172 data, status, headers = [], None, None
173 error = {
173 error = {
174 'message': str(e),
174 'message': str(e),
175 '_vcs_kind': getattr(e, '_vcs_kind', None)
175 '_vcs_kind': getattr(e, '_vcs_kind', None)
176 }
176 }
177
177
178 start_response(200, {})
178 start_response(200, {})
179 return self._iterator(error, status, headers, data)
179 return self._iterator(error, status, headers, data)
180
180
181 def _iterator(self, error, status, headers, data):
181 def _iterator(self, error, status, headers, data):
182 initial_data = [
182 initial_data = [
183 error,
183 error,
184 status,
184 status,
185 headers,
185 headers,
186 ]
186 ]
187
187
188 for d in chain(initial_data, data):
188 for d in chain(initial_data, data):
189 yield msgpack.packb(d)
189 yield msgpack.packb(d)
190
190
191
191
192 class HTTPApplication(object):
192 class HTTPApplication(object):
193 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
193 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
194
194
195 remote_wsgi = remote_wsgi
195 remote_wsgi = remote_wsgi
196 _use_echo_app = False
196 _use_echo_app = False
197
197
198 def __init__(self, settings=None, global_config=None):
198 def __init__(self, settings=None, global_config=None):
199 self._sanitize_settings_and_apply_defaults(settings)
199 self._sanitize_settings_and_apply_defaults(settings)
200
200
201 self.config = Configurator(settings=settings)
201 self.config = Configurator(settings=settings)
202 self.global_config = global_config
202 self.global_config = global_config
203 self.config.include('vcsserver.lib.rc_cache')
203 self.config.include('vcsserver.lib.rc_cache')
204
204
205 locale = settings.get('locale', '') or 'en_US.UTF-8'
205 locale = settings.get('locale', '') or 'en_US.UTF-8'
206 vcs = VCS(locale=locale, cache_config=settings)
206 vcs = VCS(locale=locale, cache_config=settings)
207 self._remotes = {
207 self._remotes = {
208 'hg': vcs._hg_remote,
208 'hg': vcs._hg_remote,
209 'git': vcs._git_remote,
209 'git': vcs._git_remote,
210 'svn': vcs._svn_remote,
210 'svn': vcs._svn_remote,
211 'server': vcs._vcsserver,
211 'server': vcs._vcsserver,
212 }
212 }
213 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
213 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
214 self._use_echo_app = True
214 self._use_echo_app = True
215 log.warning("Using EchoApp for VCS operations.")
215 log.warning("Using EchoApp for VCS operations.")
216 self.remote_wsgi = remote_wsgi_stub
216 self.remote_wsgi = remote_wsgi_stub
217 self._configure_settings(settings)
217 self._configure_settings(settings)
218 self._configure()
218 self._configure()
219
219
220 def _configure_settings(self, app_settings):
220 def _configure_settings(self, app_settings):
221 """
221 """
222 Configure the settings module.
222 Configure the settings module.
223 """
223 """
224 git_path = app_settings.get('git_path', None)
224 git_path = app_settings.get('git_path', None)
225 if git_path:
225 if git_path:
226 settings.GIT_EXECUTABLE = git_path
226 settings.GIT_EXECUTABLE = git_path
227 binary_dir = app_settings.get('core.binary_dir', None)
227 binary_dir = app_settings.get('core.binary_dir', None)
228 if binary_dir:
228 if binary_dir:
229 settings.BINARY_DIR = binary_dir
229 settings.BINARY_DIR = binary_dir
230
230
231 def _sanitize_settings_and_apply_defaults(self, settings):
231 def _sanitize_settings_and_apply_defaults(self, settings):
232 # repo_object cache
232 # repo_object cache
233 _string_setting(
233 _string_setting(
234 settings,
234 settings,
235 'rc_cache.repo_object.backend',
235 'rc_cache.repo_object.backend',
236 'dogpile.cache.rc.memory_lru')
236 'dogpile.cache.rc.memory_lru')
237 _int_setting(
237 _int_setting(
238 settings,
238 settings,
239 'rc_cache.repo_object.expiration_time',
239 'rc_cache.repo_object.expiration_time',
240 300)
240 300)
241 _int_setting(
241 _int_setting(
242 settings,
242 settings,
243 'rc_cache.repo_object.max_size',
243 'rc_cache.repo_object.max_size',
244 1024)
244 1024)
245
245
246 def _configure(self):
246 def _configure(self):
247 self.config.add_renderer(
247 self.config.add_renderer(
248 name='msgpack',
248 name='msgpack',
249 factory=self._msgpack_renderer_factory)
249 factory=self._msgpack_renderer_factory)
250
250
251 self.config.add_route('service', '/_service')
251 self.config.add_route('service', '/_service')
252 self.config.add_route('status', '/status')
252 self.config.add_route('status', '/status')
253 self.config.add_route('hg_proxy', '/proxy/hg')
253 self.config.add_route('hg_proxy', '/proxy/hg')
254 self.config.add_route('git_proxy', '/proxy/git')
254 self.config.add_route('git_proxy', '/proxy/git')
255 self.config.add_route('vcs', '/{backend}')
255 self.config.add_route('vcs', '/{backend}')
256 self.config.add_route('stream_git', '/stream/git/*repo_name')
256 self.config.add_route('stream_git', '/stream/git/*repo_name')
257 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
257 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
258
258
259 self.config.add_view(
259 self.config.add_view(
260 self.status_view, route_name='status', renderer='json')
260 self.status_view, route_name='status', renderer='json')
261 self.config.add_view(
261 self.config.add_view(
262 self.service_view, route_name='service', renderer='msgpack')
262 self.service_view, route_name='service', renderer='msgpack')
263
263
264 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
264 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
265 self.config.add_view(self.git_proxy(), route_name='git_proxy')
265 self.config.add_view(self.git_proxy(), route_name='git_proxy')
266 self.config.add_view(
266 self.config.add_view(
267 self.vcs_view, route_name='vcs', renderer='msgpack',
267 self.vcs_view, route_name='vcs', renderer='msgpack',
268 custom_predicates=[self.is_vcs_view])
268 custom_predicates=[self.is_vcs_view])
269
269
270 self.config.add_view(self.hg_stream(), route_name='stream_hg')
270 self.config.add_view(self.hg_stream(), route_name='stream_hg')
271 self.config.add_view(self.git_stream(), route_name='stream_git')
271 self.config.add_view(self.git_stream(), route_name='stream_git')
272
272
273 def notfound(request):
273 def notfound(request):
274 return {'status': '404 NOT FOUND'}
274 return {'status': '404 NOT FOUND'}
275 self.config.add_notfound_view(notfound, renderer='json')
275 self.config.add_notfound_view(notfound, renderer='json')
276
276
277 self.config.add_view(self.handle_vcs_exception, context=Exception)
277 self.config.add_view(self.handle_vcs_exception, context=Exception)
278
278
279 self.config.add_tween(
279 self.config.add_tween(
280 'vcsserver.tweens.RequestWrapperTween',
280 'vcsserver.tweens.RequestWrapperTween',
281 )
281 )
282
282
283 def wsgi_app(self):
283 def wsgi_app(self):
284 return self.config.make_wsgi_app()
284 return self.config.make_wsgi_app()
285
285
286 def vcs_view(self, request):
286 def vcs_view(self, request):
287 remote = self._remotes[request.matchdict['backend']]
287 remote = self._remotes[request.matchdict['backend']]
288 payload = msgpack.unpackb(request.body, use_list=True)
288 payload = msgpack.unpackb(request.body, use_list=True)
289 method = payload.get('method')
289 method = payload.get('method')
290 params = payload.get('params')
290 params = payload.get('params')
291 wire = params.get('wire')
291 wire = params.get('wire')
292 args = params.get('args')
292 args = params.get('args')
293 kwargs = params.get('kwargs')
293 kwargs = params.get('kwargs')
294 context_uid = None
294 context_uid = None
295
295
296 if wire:
296 if wire:
297 try:
297 try:
298 wire['context'] = context_uid = uuid.UUID(wire['context'])
298 wire['context'] = context_uid = uuid.UUID(wire['context'])
299 except KeyError:
299 except KeyError:
300 pass
300 pass
301 args.insert(0, wire)
301 args.insert(0, wire)
302
302
303 log.debug('method called:%s with kwargs:%s context_uid: %s',
303 log.debug('method called:%s with kwargs:%s context_uid: %s',
304 method, kwargs, context_uid)
304 method, kwargs, context_uid)
305 try:
305 try:
306 resp = getattr(remote, method)(*args, **kwargs)
306 resp = getattr(remote, method)(*args, **kwargs)
307 except Exception as e:
307 except Exception as e:
308 exc_info = list(sys.exc_info())
308 exc_info = list(sys.exc_info())
309 exc_type, exc_value, exc_traceback = exc_info
309 exc_type, exc_value, exc_traceback = exc_info
310
310
311 org_exc = getattr(e, '_org_exc', None)
311 org_exc = getattr(e, '_org_exc', None)
312 org_exc_name = None
312 org_exc_name = None
313 if org_exc:
313 if org_exc:
314 org_exc_name = org_exc.__class__.__name__
314 org_exc_name = org_exc.__class__.__name__
315 # replace our "faked" exception with our org
315 # replace our "faked" exception with our org
316 exc_info[0] = org_exc.__class__
316 exc_info[0] = org_exc.__class__
317 exc_info[1] = org_exc
317 exc_info[1] = org_exc
318
318
319 store_exception(id(exc_info), exc_info)
319 store_exception(id(exc_info), exc_info)
320
320
321 tb_info = ''.join(
321 tb_info = ''.join(
322 traceback.format_exception(exc_type, exc_value, exc_traceback))
322 traceback.format_exception(exc_type, exc_value, exc_traceback))
323
323
324 type_ = e.__class__.__name__
324 type_ = e.__class__.__name__
325 if type_ not in self.ALLOWED_EXCEPTIONS:
325 if type_ not in self.ALLOWED_EXCEPTIONS:
326 type_ = None
326 type_ = None
327
327
328 resp = {
328 resp = {
329 'id': payload.get('id'),
329 'id': payload.get('id'),
330 'error': {
330 'error': {
331 'message': e.message,
331 'message': e.message,
332 'traceback': tb_info,
332 'traceback': tb_info,
333 'org_exc': org_exc_name,
333 'org_exc': org_exc_name,
334 'type': type_
334 'type': type_
335 }
335 }
336 }
336 }
337 try:
337 try:
338 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
338 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
339 except AttributeError:
339 except AttributeError:
340 pass
340 pass
341 else:
341 else:
342 resp = {
342 resp = {
343 'id': payload.get('id'),
343 'id': payload.get('id'),
344 'result': resp
344 'result': resp
345 }
345 }
346
346
347 return resp
347 return resp
348
348
349 def status_view(self, request):
349 def status_view(self, request):
350 import vcsserver
350 import vcsserver
351 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
351 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
352 'pid': os.getpid()}
352 'pid': os.getpid()}
353
353
354 def service_view(self, request):
354 def service_view(self, request):
355 import vcsserver
355 import vcsserver
356
356
357 payload = msgpack.unpackb(request.body, use_list=True)
357 payload = msgpack.unpackb(request.body, use_list=True)
358
358
359 try:
359 try:
360 path = self.global_config['__file__']
360 path = self.global_config['__file__']
361 config = configparser.ConfigParser()
361 config = configparser.ConfigParser()
362 config.read(path)
362 config.read(path)
363 parsed_ini = config
363 parsed_ini = config
364 if parsed_ini.has_section('server:main'):
364 if parsed_ini.has_section('server:main'):
365 parsed_ini = dict(parsed_ini.items('server:main'))
365 parsed_ini = dict(parsed_ini.items('server:main'))
366 except Exception:
366 except Exception:
367 log.exception('Failed to read .ini file for display')
367 log.exception('Failed to read .ini file for display')
368 parsed_ini = {}
368 parsed_ini = {}
369
369
370 resp = {
370 resp = {
371 'id': payload.get('id'),
371 'id': payload.get('id'),
372 'result': dict(
372 'result': dict(
373 version=vcsserver.__version__,
373 version=vcsserver.__version__,
374 config=parsed_ini,
374 config=parsed_ini,
375 payload=payload,
375 payload=payload,
376 )
376 )
377 }
377 }
378 return resp
378 return resp
379
379
380 def _msgpack_renderer_factory(self, info):
380 def _msgpack_renderer_factory(self, info):
381 def _render(value, system):
381 def _render(value, system):
382 value = msgpack.packb(value)
382 value = msgpack.packb(value)
383 request = system.get('request')
383 request = system.get('request')
384 if request is not None:
384 if request is not None:
385 response = request.response
385 response = request.response
386 ct = response.content_type
386 ct = response.content_type
387 if ct == response.default_content_type:
387 if ct == response.default_content_type:
388 response.content_type = 'application/x-msgpack'
388 response.content_type = 'application/x-msgpack'
389 return value
389 return value
390 return _render
390 return _render
391
391
392 def set_env_from_config(self, environ, config):
392 def set_env_from_config(self, environ, config):
393 dict_conf = {}
393 dict_conf = {}
394 try:
394 try:
395 for elem in config:
395 for elem in config:
396 if elem[0] == 'rhodecode':
396 if elem[0] == 'rhodecode':
397 dict_conf = json.loads(elem[2])
397 dict_conf = json.loads(elem[2])
398 break
398 break
399 except Exception:
399 except Exception:
400 log.exception('Failed to fetch SCM CONFIG')
400 log.exception('Failed to fetch SCM CONFIG')
401 return
401 return
402
402
403 username = dict_conf.get('username')
403 username = dict_conf.get('username')
404 if username:
404 if username:
405 environ['REMOTE_USER'] = username
405 environ['REMOTE_USER'] = username
406 # mercurial specific, some extension api rely on this
406 # mercurial specific, some extension api rely on this
407 environ['HGUSER'] = username
407 environ['HGUSER'] = username
408
408
409 ip = dict_conf.get('ip')
409 ip = dict_conf.get('ip')
410 if ip:
410 if ip:
411 environ['REMOTE_HOST'] = ip
411 environ['REMOTE_HOST'] = ip
412
412
413 if _is_request_chunked(environ):
413 if _is_request_chunked(environ):
414 # set the compatibility flag for webob
414 # set the compatibility flag for webob
415 environ['wsgi.input_terminated'] = True
415 environ['wsgi.input_terminated'] = True
416
416
417 def hg_proxy(self):
417 def hg_proxy(self):
418 @wsgiapp
418 @wsgiapp
419 def _hg_proxy(environ, start_response):
419 def _hg_proxy(environ, start_response):
420 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
420 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
421 return app(environ, start_response)
421 return app(environ, start_response)
422 return _hg_proxy
422 return _hg_proxy
423
423
424 def git_proxy(self):
424 def git_proxy(self):
425 @wsgiapp
425 @wsgiapp
426 def _git_proxy(environ, start_response):
426 def _git_proxy(environ, start_response):
427 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
427 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
428 return app(environ, start_response)
428 return app(environ, start_response)
429 return _git_proxy
429 return _git_proxy
430
430
431 def hg_stream(self):
431 def hg_stream(self):
432 if self._use_echo_app:
432 if self._use_echo_app:
433 @wsgiapp
433 @wsgiapp
434 def _hg_stream(environ, start_response):
434 def _hg_stream(environ, start_response):
435 app = EchoApp('fake_path', 'fake_name', None)
435 app = EchoApp('fake_path', 'fake_name', None)
436 return app(environ, start_response)
436 return app(environ, start_response)
437 return _hg_stream
437 return _hg_stream
438 else:
438 else:
439 @wsgiapp
439 @wsgiapp
440 def _hg_stream(environ, start_response):
440 def _hg_stream(environ, start_response):
441 log.debug('http-app: handling hg stream')
441 log.debug('http-app: handling hg stream')
442 repo_path = environ['HTTP_X_RC_REPO_PATH']
442 repo_path = environ['HTTP_X_RC_REPO_PATH']
443 repo_name = environ['HTTP_X_RC_REPO_NAME']
443 repo_name = environ['HTTP_X_RC_REPO_NAME']
444 packed_config = base64.b64decode(
444 packed_config = base64.b64decode(
445 environ['HTTP_X_RC_REPO_CONFIG'])
445 environ['HTTP_X_RC_REPO_CONFIG'])
446 config = msgpack.unpackb(packed_config)
446 config = msgpack.unpackb(packed_config)
447 app = scm_app.create_hg_wsgi_app(
447 app = scm_app.create_hg_wsgi_app(
448 repo_path, repo_name, config)
448 repo_path, repo_name, config)
449
449
450 # Consistent path information for hgweb
450 # Consistent path information for hgweb
451 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
451 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
452 environ['REPO_NAME'] = repo_name
452 environ['REPO_NAME'] = repo_name
453 self.set_env_from_config(environ, config)
453 self.set_env_from_config(environ, config)
454
454
455 log.debug('http-app: starting app handler '
455 log.debug('http-app: starting app handler '
456 'with %s and process request', app)
456 'with %s and process request', app)
457 return app(environ, ResponseFilter(start_response))
457 return app(environ, ResponseFilter(start_response))
458 return _hg_stream
458 return _hg_stream
459
459
460 def git_stream(self):
460 def git_stream(self):
461 if self._use_echo_app:
461 if self._use_echo_app:
462 @wsgiapp
462 @wsgiapp
463 def _git_stream(environ, start_response):
463 def _git_stream(environ, start_response):
464 app = EchoApp('fake_path', 'fake_name', None)
464 app = EchoApp('fake_path', 'fake_name', None)
465 return app(environ, start_response)
465 return app(environ, start_response)
466 return _git_stream
466 return _git_stream
467 else:
467 else:
468 @wsgiapp
468 @wsgiapp
469 def _git_stream(environ, start_response):
469 def _git_stream(environ, start_response):
470 log.debug('http-app: handling git stream')
470 log.debug('http-app: handling git stream')
471 repo_path = environ['HTTP_X_RC_REPO_PATH']
471 repo_path = environ['HTTP_X_RC_REPO_PATH']
472 repo_name = environ['HTTP_X_RC_REPO_NAME']
472 repo_name = environ['HTTP_X_RC_REPO_NAME']
473 packed_config = base64.b64decode(
473 packed_config = base64.b64decode(
474 environ['HTTP_X_RC_REPO_CONFIG'])
474 environ['HTTP_X_RC_REPO_CONFIG'])
475 config = msgpack.unpackb(packed_config)
475 config = msgpack.unpackb(packed_config)
476
476
477 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
477 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
478 self.set_env_from_config(environ, config)
478 self.set_env_from_config(environ, config)
479
479
480 content_type = environ.get('CONTENT_TYPE', '')
480 content_type = environ.get('CONTENT_TYPE', '')
481
481
482 path = environ['PATH_INFO']
482 path = environ['PATH_INFO']
483 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
483 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
484 log.debug(
484 log.debug(
485 'LFS: Detecting if request `%s` is LFS server path based '
485 'LFS: Detecting if request `%s` is LFS server path based '
486 'on content type:`%s`, is_lfs:%s',
486 'on content type:`%s`, is_lfs:%s',
487 path, content_type, is_lfs_request)
487 path, content_type, is_lfs_request)
488
488
489 if not is_lfs_request:
489 if not is_lfs_request:
490 # fallback detection by path
490 # fallback detection by path
491 if GIT_LFS_PROTO_PAT.match(path):
491 if GIT_LFS_PROTO_PAT.match(path):
492 is_lfs_request = True
492 is_lfs_request = True
493 log.debug(
493 log.debug(
494 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
494 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
495 path, is_lfs_request)
495 path, is_lfs_request)
496
496
497 if is_lfs_request:
497 if is_lfs_request:
498 app = scm_app.create_git_lfs_wsgi_app(
498 app = scm_app.create_git_lfs_wsgi_app(
499 repo_path, repo_name, config)
499 repo_path, repo_name, config)
500 else:
500 else:
501 app = scm_app.create_git_wsgi_app(
501 app = scm_app.create_git_wsgi_app(
502 repo_path, repo_name, config)
502 repo_path, repo_name, config)
503
503
504 log.debug('http-app: starting app handler '
504 log.debug('http-app: starting app handler '
505 'with %s and process request', app)
505 'with %s and process request', app)
506
506
507 return app(environ, start_response)
507 return app(environ, start_response)
508
508
509 return _git_stream
509 return _git_stream
510
510
511 def is_vcs_view(self, context, request):
511 def is_vcs_view(self, context, request):
512 """
512 """
513 View predicate that returns true if given backend is supported by
513 View predicate that returns true if given backend is supported by
514 defined remotes.
514 defined remotes.
515 """
515 """
516 backend = request.matchdict.get('backend')
516 backend = request.matchdict.get('backend')
517 return backend in self._remotes
517 return backend in self._remotes
518
518
519 def handle_vcs_exception(self, exception, request):
519 def handle_vcs_exception(self, exception, request):
520 _vcs_kind = getattr(exception, '_vcs_kind', '')
520 _vcs_kind = getattr(exception, '_vcs_kind', '')
521 if _vcs_kind == 'repo_locked':
521 if _vcs_kind == 'repo_locked':
522 # Get custom repo-locked status code if present.
522 # Get custom repo-locked status code if present.
523 status_code = request.headers.get('X-RC-Locked-Status-Code')
523 status_code = request.headers.get('X-RC-Locked-Status-Code')
524 return HTTPRepoLocked(
524 return HTTPRepoLocked(
525 title=exception.message, status_code=status_code)
525 title=exception.message, status_code=status_code)
526
526
527 elif _vcs_kind == 'repo_branch_protected':
528 # Get custom repo-branch-protected status code if present.
529 return HTTPRepoBranchProtected(title=exception.message)
530
527 exc_info = request.exc_info
531 exc_info = request.exc_info
528 store_exception(id(exc_info), exc_info)
532 store_exception(id(exc_info), exc_info)
529
533
530 traceback_info = 'unavailable'
534 traceback_info = 'unavailable'
531 if request.exc_info:
535 if request.exc_info:
532 exc_type, exc_value, exc_tb = request.exc_info
536 exc_type, exc_value, exc_tb = request.exc_info
533 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
537 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
534
538
535 log.error(
539 log.error(
536 'error occurred handling this request for path: %s, \n tb: %s',
540 'error occurred handling this request for path: %s, \n tb: %s',
537 request.path, traceback_info)
541 request.path, traceback_info)
538 raise exception
542 raise exception
539
543
540
544
541 class ResponseFilter(object):
545 class ResponseFilter(object):
542
546
543 def __init__(self, start_response):
547 def __init__(self, start_response):
544 self._start_response = start_response
548 self._start_response = start_response
545
549
546 def __call__(self, status, response_headers, exc_info=None):
550 def __call__(self, status, response_headers, exc_info=None):
547 headers = tuple(
551 headers = tuple(
548 (h, v) for h, v in response_headers
552 (h, v) for h, v in response_headers
549 if not wsgiref.util.is_hop_by_hop(h))
553 if not wsgiref.util.is_hop_by_hop(h))
550 return self._start_response(status, headers, exc_info)
554 return self._start_response(status, headers, exc_info)
551
555
552
556
553 def main(global_config, **settings):
557 def main(global_config, **settings):
554 if MercurialFactory:
558 if MercurialFactory:
555 hgpatches.patch_largefiles_capabilities()
559 hgpatches.patch_largefiles_capabilities()
556 hgpatches.patch_subrepo_type_mapping()
560 hgpatches.patch_subrepo_type_mapping()
557
561
558 app = HTTPApplication(settings=settings, global_config=global_config)
562 app = HTTPApplication(settings=settings, global_config=global_config)
559 return app.wsgi_app()
563 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now