##// END OF EJS Templates
Added custom error.
ilin.s -
r1283:57090f1d default
parent child Browse files
Show More
@@ -1,125 +1,131 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-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 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 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
28
28
29
29
30 def _make_exception(kind, org_exc, *args):
30 def _make_exception(kind, org_exc, *args):
31 """
31 """
32 Prepares a base `Exception` instance to be sent over the wire.
32 Prepares a base `Exception` instance to be sent over the wire.
33
33
34 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
35 `_vcs_kind` to the exception.
35 `_vcs_kind` to the exception.
36 """
36 """
37 exc = Exception(*args)
37 exc = Exception(*args)
38 exc._vcs_kind = kind
38 exc._vcs_kind = kind
39 exc._org_exc = org_exc
39 exc._org_exc = org_exc
40 exc._org_exc_tb = getattr(org_exc, '_org_exc_tb', '')
40 exc._org_exc_tb = getattr(org_exc, '_org_exc_tb', '')
41 return exc
41 return exc
42
42
43
43
44 def AbortException(org_exc=None):
44 def AbortException(org_exc=None):
45 def _make_exception_wrapper(*args):
45 def _make_exception_wrapper(*args):
46 return _make_exception('abort', org_exc, *args)
46 return _make_exception('abort', org_exc, *args)
47 return _make_exception_wrapper
47 return _make_exception_wrapper
48
48
49
49
50 def ArchiveException(org_exc=None):
50 def ArchiveException(org_exc=None):
51 def _make_exception_wrapper(*args):
51 def _make_exception_wrapper(*args):
52 return _make_exception('archive', org_exc, *args)
52 return _make_exception('archive', org_exc, *args)
53 return _make_exception_wrapper
53 return _make_exception_wrapper
54
54
55
55
56 def ClientNotSupportedException(org_exc=None):
57 def _make_exception_wrapper(*args):
58 return _make_exception('client_not_supported', org_exc, *args)
59 return _make_exception_wrapper
60
61
56 def LookupException(org_exc=None):
62 def LookupException(org_exc=None):
57 def _make_exception_wrapper(*args):
63 def _make_exception_wrapper(*args):
58 return _make_exception('lookup', org_exc, *args)
64 return _make_exception('lookup', org_exc, *args)
59 return _make_exception_wrapper
65 return _make_exception_wrapper
60
66
61
67
62 def VcsException(org_exc=None):
68 def VcsException(org_exc=None):
63 def _make_exception_wrapper(*args):
69 def _make_exception_wrapper(*args):
64 return _make_exception('error', org_exc, *args)
70 return _make_exception('error', org_exc, *args)
65 return _make_exception_wrapper
71 return _make_exception_wrapper
66
72
67
73
68 def RepositoryLockedException(org_exc=None):
74 def RepositoryLockedException(org_exc=None):
69 def _make_exception_wrapper(*args):
75 def _make_exception_wrapper(*args):
70 return _make_exception('repo_locked', org_exc, *args)
76 return _make_exception('repo_locked', org_exc, *args)
71 return _make_exception_wrapper
77 return _make_exception_wrapper
72
78
73
79
74 def RepositoryBranchProtectedException(org_exc=None):
80 def RepositoryBranchProtectedException(org_exc=None):
75 def _make_exception_wrapper(*args):
81 def _make_exception_wrapper(*args):
76 return _make_exception('repo_branch_protected', org_exc, *args)
82 return _make_exception('repo_branch_protected', org_exc, *args)
77 return _make_exception_wrapper
83 return _make_exception_wrapper
78
84
79
85
80 def RequirementException(org_exc=None):
86 def RequirementException(org_exc=None):
81 def _make_exception_wrapper(*args):
87 def _make_exception_wrapper(*args):
82 return _make_exception('requirement', org_exc, *args)
88 return _make_exception('requirement', org_exc, *args)
83 return _make_exception_wrapper
89 return _make_exception_wrapper
84
90
85
91
86 def UnhandledException(org_exc=None):
92 def UnhandledException(org_exc=None):
87 def _make_exception_wrapper(*args):
93 def _make_exception_wrapper(*args):
88 return _make_exception('unhandled', org_exc, *args)
94 return _make_exception('unhandled', org_exc, *args)
89 return _make_exception_wrapper
95 return _make_exception_wrapper
90
96
91
97
92 def URLError(org_exc=None):
98 def URLError(org_exc=None):
93 def _make_exception_wrapper(*args):
99 def _make_exception_wrapper(*args):
94 return _make_exception('url_error', org_exc, *args)
100 return _make_exception('url_error', org_exc, *args)
95 return _make_exception_wrapper
101 return _make_exception_wrapper
96
102
97
103
98 def SubrepoMergeException(org_exc=None):
104 def SubrepoMergeException(org_exc=None):
99 def _make_exception_wrapper(*args):
105 def _make_exception_wrapper(*args):
100 return _make_exception('subrepo_merge_error', org_exc, *args)
106 return _make_exception('subrepo_merge_error', org_exc, *args)
101 return _make_exception_wrapper
107 return _make_exception_wrapper
102
108
103
109
104 class HTTPRepoLocked(HTTPLocked):
110 class HTTPRepoLocked(HTTPLocked):
105 """
111 """
106 Subclass of HTTPLocked response that allows to set the title and status
112 Subclass of HTTPLocked response that allows to set the title and status
107 code via constructor arguments.
113 code via constructor arguments.
108 """
114 """
109 def __init__(self, title, status_code=None, **kwargs):
115 def __init__(self, title, status_code=None, **kwargs):
110 self.code = status_code or HTTPLocked.code
116 self.code = status_code or HTTPLocked.code
111 self.title = title
117 self.title = title
112 super().__init__(**kwargs)
118 super().__init__(**kwargs)
113
119
114
120
115 class HTTPRepoBranchProtected(HTTPForbidden):
121 class HTTPRepoBranchProtected(HTTPForbidden):
116 def __init__(self, *args, **kwargs):
122 def __init__(self, *args, **kwargs):
117 super(HTTPForbidden, self).__init__(*args, **kwargs)
123 super(HTTPForbidden, self).__init__(*args, **kwargs)
118
124
119
125
120 class RefNotFoundException(KeyError):
126 class RefNotFoundException(KeyError):
121 pass
127 pass
122
128
123
129
124 class NoContentException(ValueError):
130 class NoContentException(ValueError):
125 pass
131 pass
@@ -1,822 +1,824 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-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 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 io
18 import io
19 import os
19 import os
20 import sys
20 import sys
21 import logging
21 import logging
22 import collections
22 import collections
23 import base64
23 import base64
24 import msgpack
24 import msgpack
25 import dataclasses
25 import dataclasses
26 import pygit2
26 import pygit2
27
27
28 import http.client
28 import http.client
29 from celery import Celery
29 from celery import Celery
30
30
31 import mercurial.scmutil
31 import mercurial.scmutil
32 import mercurial.node
32 import mercurial.node
33
33
34 from vcsserver import exceptions, subprocessio, settings
34 from vcsserver import exceptions, subprocessio, settings
35 from vcsserver.lib.ext_json import json
35 from vcsserver.lib.ext_json import json
36 from vcsserver.lib.str_utils import ascii_str, safe_str
36 from vcsserver.lib.str_utils import ascii_str, safe_str
37 from vcsserver.lib.svn_txn_utils import get_txn_id_from_store
37 from vcsserver.lib.svn_txn_utils import get_txn_id_from_store
38 from vcsserver.remote.git_remote import Repository
38 from vcsserver.remote.git_remote import Repository
39
39
40 celery_app = Celery('__vcsserver__')
40 celery_app = Celery('__vcsserver__')
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class HooksHttpClient:
44 class HooksHttpClient:
45 proto = 'msgpack.v1'
45 proto = 'msgpack.v1'
46 connection = None
46 connection = None
47
47
48 def __init__(self, hooks_uri):
48 def __init__(self, hooks_uri):
49 self.hooks_uri = hooks_uri
49 self.hooks_uri = hooks_uri
50
50
51 def __repr__(self):
51 def __repr__(self):
52 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
52 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
53
53
54 def __call__(self, method, extras):
54 def __call__(self, method, extras):
55 connection = http.client.HTTPConnection(self.hooks_uri)
55 connection = http.client.HTTPConnection(self.hooks_uri)
56 # binary msgpack body
56 # binary msgpack body
57 headers, body = self._serialize(method, extras)
57 headers, body = self._serialize(method, extras)
58 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
58 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
59
59
60 try:
60 try:
61 try:
61 try:
62 connection.request('POST', '/', body, headers)
62 connection.request('POST', '/', body, headers)
63 except Exception as error:
63 except Exception as error:
64 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
64 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
65 raise
65 raise
66
66
67 response = connection.getresponse()
67 response = connection.getresponse()
68 try:
68 try:
69 return msgpack.load(response)
69 return msgpack.load(response)
70 except Exception:
70 except Exception:
71 response_data = response.read()
71 response_data = response.read()
72 log.exception('Failed to decode hook response json data. '
72 log.exception('Failed to decode hook response json data. '
73 'response_code:%s, raw_data:%s',
73 'response_code:%s, raw_data:%s',
74 response.status, response_data)
74 response.status, response_data)
75 raise
75 raise
76 finally:
76 finally:
77 connection.close()
77 connection.close()
78
78
79 @classmethod
79 @classmethod
80 def _serialize(cls, hook_name, extras):
80 def _serialize(cls, hook_name, extras):
81 data = {
81 data = {
82 'method': hook_name,
82 'method': hook_name,
83 'extras': extras
83 'extras': extras
84 }
84 }
85 headers = {
85 headers = {
86 "rc-hooks-protocol": cls.proto,
86 "rc-hooks-protocol": cls.proto,
87 "Connection": "keep-alive"
87 "Connection": "keep-alive"
88 }
88 }
89 return headers, msgpack.packb(data)
89 return headers, msgpack.packb(data)
90
90
91
91
92 class HooksCeleryClient:
92 class HooksCeleryClient:
93 TASK_TIMEOUT = 60 # time in seconds
93 TASK_TIMEOUT = 60 # time in seconds
94
94
95 def __init__(self, queue, backend):
95 def __init__(self, queue, backend):
96 celery_app.config_from_object({
96 celery_app.config_from_object({
97 'broker_url': queue, 'result_backend': backend,
97 'broker_url': queue, 'result_backend': backend,
98 'broker_connection_retry_on_startup': True,
98 'broker_connection_retry_on_startup': True,
99 'task_serializer': 'json',
99 'task_serializer': 'json',
100 'accept_content': ['json', 'msgpack'],
100 'accept_content': ['json', 'msgpack'],
101 'result_serializer': 'json',
101 'result_serializer': 'json',
102 'result_accept_content': ['json', 'msgpack']
102 'result_accept_content': ['json', 'msgpack']
103 })
103 })
104 self.celery_app = celery_app
104 self.celery_app = celery_app
105
105
106 def __call__(self, method, extras):
106 def __call__(self, method, extras):
107 inquired_task = self.celery_app.signature(
107 inquired_task = self.celery_app.signature(
108 f'rhodecode.lib.celerylib.tasks.{method}'
108 f'rhodecode.lib.celerylib.tasks.{method}'
109 )
109 )
110 return inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
110 return inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
111
111
112
112
113 class HooksShadowRepoClient:
113 class HooksShadowRepoClient:
114
114
115 def __call__(self, hook_name, extras):
115 def __call__(self, hook_name, extras):
116 return {'output': '', 'status': 0}
116 return {'output': '', 'status': 0}
117
117
118
118
119 class RemoteMessageWriter:
119 class RemoteMessageWriter:
120 """Writer base class."""
120 """Writer base class."""
121 def write(self, message):
121 def write(self, message):
122 raise NotImplementedError()
122 raise NotImplementedError()
123
123
124
124
125 class HgMessageWriter(RemoteMessageWriter):
125 class HgMessageWriter(RemoteMessageWriter):
126 """Writer that knows how to send messages to mercurial clients."""
126 """Writer that knows how to send messages to mercurial clients."""
127
127
128 def __init__(self, ui):
128 def __init__(self, ui):
129 self.ui = ui
129 self.ui = ui
130
130
131 def write(self, message: str):
131 def write(self, message: str):
132 # TODO: Check why the quiet flag is set by default.
132 # TODO: Check why the quiet flag is set by default.
133 old = self.ui.quiet
133 old = self.ui.quiet
134 self.ui.quiet = False
134 self.ui.quiet = False
135 self.ui.status(message.encode('utf-8'))
135 self.ui.status(message.encode('utf-8'))
136 self.ui.quiet = old
136 self.ui.quiet = old
137
137
138
138
139 class GitMessageWriter(RemoteMessageWriter):
139 class GitMessageWriter(RemoteMessageWriter):
140 """Writer that knows how to send messages to git clients."""
140 """Writer that knows how to send messages to git clients."""
141
141
142 def __init__(self, stdout=None):
142 def __init__(self, stdout=None):
143 self.stdout = stdout or sys.stdout
143 self.stdout = stdout or sys.stdout
144
144
145 def write(self, message: str):
145 def write(self, message: str):
146 self.stdout.write(message)
146 self.stdout.write(message)
147
147
148
148
149 class SvnMessageWriter(RemoteMessageWriter):
149 class SvnMessageWriter(RemoteMessageWriter):
150 """Writer that knows how to send messages to svn clients."""
150 """Writer that knows how to send messages to svn clients."""
151
151
152 def __init__(self, stderr=None):
152 def __init__(self, stderr=None):
153 # SVN needs data sent to stderr for back-to-client messaging
153 # SVN needs data sent to stderr for back-to-client messaging
154 self.stderr = stderr or sys.stderr
154 self.stderr = stderr or sys.stderr
155
155
156 def write(self, message):
156 def write(self, message):
157 self.stderr.write(message)
157 self.stderr.write(message)
158
158
159
159
160 def _handle_exception(result):
160 def _handle_exception(result):
161 exception_class = result.get('exception')
161 exception_class = result.get('exception')
162 exception_traceback = result.get('exception_traceback')
162 exception_traceback = result.get('exception_traceback')
163 log.debug('Handling hook-call exception: %s', exception_class)
163 log.debug('Handling hook-call exception: %s', exception_class)
164
164
165 if exception_traceback:
165 if exception_traceback:
166 log.error('Got traceback from remote call:%s', exception_traceback)
166 log.error('Got traceback from remote call:%s', exception_traceback)
167
167
168 if exception_class == 'HTTPLockedRC':
168 if exception_class == 'HTTPLockedRC':
169 raise exceptions.RepositoryLockedException()(*result['exception_args'])
169 raise exceptions.RepositoryLockedException()(*result['exception_args'])
170 elif exception_class == 'ClientNotSupportedError':
171 raise exceptions.ClientNotSupportedException()(*result['exception_args'])
170 elif exception_class == 'HTTPBranchProtected':
172 elif exception_class == 'HTTPBranchProtected':
171 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
173 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
172 elif exception_class == 'RepositoryError':
174 elif exception_class == 'RepositoryError':
173 raise exceptions.VcsException()(*result['exception_args'])
175 raise exceptions.VcsException()(*result['exception_args'])
174 elif exception_class:
176 elif exception_class:
175 raise Exception(
177 raise Exception(
176 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
178 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
177 )
179 )
178
180
179
181
180 def _get_hooks_client(extras):
182 def _get_hooks_client(extras):
181 hooks_uri = extras.get('hooks_uri')
183 hooks_uri = extras.get('hooks_uri')
182 task_queue = extras.get('task_queue')
184 task_queue = extras.get('task_queue')
183 task_backend = extras.get('task_backend')
185 task_backend = extras.get('task_backend')
184 is_shadow_repo = extras.get('is_shadow_repo')
186 is_shadow_repo = extras.get('is_shadow_repo')
185
187
186 if hooks_uri:
188 if hooks_uri:
187 return HooksHttpClient(hooks_uri)
189 return HooksHttpClient(hooks_uri)
188 elif task_queue and task_backend:
190 elif task_queue and task_backend:
189 return HooksCeleryClient(task_queue, task_backend)
191 return HooksCeleryClient(task_queue, task_backend)
190 elif is_shadow_repo:
192 elif is_shadow_repo:
191 return HooksShadowRepoClient()
193 return HooksShadowRepoClient()
192 else:
194 else:
193 raise Exception("Hooks client not found!")
195 raise Exception("Hooks client not found!")
194
196
195
197
196 def _call_hook(hook_name, extras, writer):
198 def _call_hook(hook_name, extras, writer):
197 hooks_client = _get_hooks_client(extras)
199 hooks_client = _get_hooks_client(extras)
198 log.debug('Hooks, using client:%s', hooks_client)
200 log.debug('Hooks, using client:%s', hooks_client)
199 result = hooks_client(hook_name, extras)
201 result = hooks_client(hook_name, extras)
200 log.debug('Hooks got result: %s', result)
202 log.debug('Hooks got result: %s', result)
201 _handle_exception(result)
203 _handle_exception(result)
202 writer.write(result['output'])
204 writer.write(result['output'])
203
205
204 return result['status']
206 return result['status']
205
207
206
208
207 def _extras_from_ui(ui):
209 def _extras_from_ui(ui):
208 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
210 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
209 if not hook_data:
211 if not hook_data:
210 # maybe it's inside environ ?
212 # maybe it's inside environ ?
211 env_hook_data = os.environ.get('RC_SCM_DATA')
213 env_hook_data = os.environ.get('RC_SCM_DATA')
212 if env_hook_data:
214 if env_hook_data:
213 hook_data = env_hook_data
215 hook_data = env_hook_data
214
216
215 extras = {}
217 extras = {}
216 if hook_data:
218 if hook_data:
217 extras = json.loads(hook_data)
219 extras = json.loads(hook_data)
218 return extras
220 return extras
219
221
220
222
221 def _rev_range_hash(repo, node, check_heads=False):
223 def _rev_range_hash(repo, node, check_heads=False):
222 from vcsserver.hgcompat import get_ctx
224 from vcsserver.hgcompat import get_ctx
223
225
224 commits = []
226 commits = []
225 revs = []
227 revs = []
226 start = get_ctx(repo, node).rev()
228 start = get_ctx(repo, node).rev()
227 end = len(repo)
229 end = len(repo)
228 for rev in range(start, end):
230 for rev in range(start, end):
229 revs.append(rev)
231 revs.append(rev)
230 ctx = get_ctx(repo, rev)
232 ctx = get_ctx(repo, rev)
231 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
233 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
232 branch = safe_str(ctx.branch())
234 branch = safe_str(ctx.branch())
233 commits.append((commit_id, branch))
235 commits.append((commit_id, branch))
234
236
235 parent_heads = []
237 parent_heads = []
236 if check_heads:
238 if check_heads:
237 parent_heads = _check_heads(repo, start, end, revs)
239 parent_heads = _check_heads(repo, start, end, revs)
238 return commits, parent_heads
240 return commits, parent_heads
239
241
240
242
241 def _check_heads(repo, start, end, commits):
243 def _check_heads(repo, start, end, commits):
242 from vcsserver.hgcompat import get_ctx
244 from vcsserver.hgcompat import get_ctx
243 changelog = repo.changelog
245 changelog = repo.changelog
244 parents = set()
246 parents = set()
245
247
246 for new_rev in commits:
248 for new_rev in commits:
247 for p in changelog.parentrevs(new_rev):
249 for p in changelog.parentrevs(new_rev):
248 if p == mercurial.node.nullrev:
250 if p == mercurial.node.nullrev:
249 continue
251 continue
250 if p < start:
252 if p < start:
251 parents.add(p)
253 parents.add(p)
252
254
253 for p in parents:
255 for p in parents:
254 branch = get_ctx(repo, p).branch()
256 branch = get_ctx(repo, p).branch()
255 # The heads descending from that parent, on the same branch
257 # The heads descending from that parent, on the same branch
256 parent_heads = {p}
258 parent_heads = {p}
257 reachable = {p}
259 reachable = {p}
258 for x in range(p + 1, end):
260 for x in range(p + 1, end):
259 if get_ctx(repo, x).branch() != branch:
261 if get_ctx(repo, x).branch() != branch:
260 continue
262 continue
261 for pp in changelog.parentrevs(x):
263 for pp in changelog.parentrevs(x):
262 if pp in reachable:
264 if pp in reachable:
263 reachable.add(x)
265 reachable.add(x)
264 parent_heads.discard(pp)
266 parent_heads.discard(pp)
265 parent_heads.add(x)
267 parent_heads.add(x)
266 # More than one head? Suggest merging
268 # More than one head? Suggest merging
267 if len(parent_heads) > 1:
269 if len(parent_heads) > 1:
268 return list(parent_heads)
270 return list(parent_heads)
269
271
270 return []
272 return []
271
273
272
274
273 def _get_git_env():
275 def _get_git_env():
274 env = {}
276 env = {}
275 for k, v in os.environ.items():
277 for k, v in os.environ.items():
276 if k.startswith('GIT'):
278 if k.startswith('GIT'):
277 env[k] = v
279 env[k] = v
278
280
279 # serialized version
281 # serialized version
280 return [(k, v) for k, v in env.items()]
282 return [(k, v) for k, v in env.items()]
281
283
282
284
283 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
285 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
284 env = {}
286 env = {}
285 for k, v in os.environ.items():
287 for k, v in os.environ.items():
286 if k.startswith('HG'):
288 if k.startswith('HG'):
287 env[k] = v
289 env[k] = v
288
290
289 env['HG_NODE'] = old_rev
291 env['HG_NODE'] = old_rev
290 env['HG_NODE_LAST'] = new_rev
292 env['HG_NODE_LAST'] = new_rev
291 env['HG_TXNID'] = txnid
293 env['HG_TXNID'] = txnid
292 env['HG_PENDING'] = repo_path
294 env['HG_PENDING'] = repo_path
293
295
294 return [(k, v) for k, v in env.items()]
296 return [(k, v) for k, v in env.items()]
295
297
296
298
297 def _get_ini_settings(ini_file):
299 def _get_ini_settings(ini_file):
298 from vcsserver.http_main import sanitize_settings_and_apply_defaults
300 from vcsserver.http_main import sanitize_settings_and_apply_defaults
299 from vcsserver.lib.config_utils import get_app_config_lightweight, configure_and_store_settings
301 from vcsserver.lib.config_utils import get_app_config_lightweight, configure_and_store_settings
300
302
301 global_config = {'__file__': ini_file}
303 global_config = {'__file__': ini_file}
302 ini_settings = get_app_config_lightweight(ini_file)
304 ini_settings = get_app_config_lightweight(ini_file)
303 sanitize_settings_and_apply_defaults(global_config, ini_settings)
305 sanitize_settings_and_apply_defaults(global_config, ini_settings)
304 configure_and_store_settings(global_config, ini_settings)
306 configure_and_store_settings(global_config, ini_settings)
305
307
306 return ini_settings
308 return ini_settings
307
309
308
310
309 def _fix_hooks_executables(ini_path=''):
311 def _fix_hooks_executables(ini_path=''):
310 """
312 """
311 This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns
313 This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns
312 especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail
314 especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail
313 because svn is not on PATH
315 because svn is not on PATH
314 """
316 """
315 # set defaults, in case we can't read from ini_file
317 # set defaults, in case we can't read from ini_file
316 core_binary_dir = settings.BINARY_DIR or '/usr/local/bin/rhodecode_bin/vcs_bin'
318 core_binary_dir = settings.BINARY_DIR or '/usr/local/bin/rhodecode_bin/vcs_bin'
317 if ini_path:
319 if ini_path:
318 ini_settings = _get_ini_settings(ini_path)
320 ini_settings = _get_ini_settings(ini_path)
319 core_binary_dir = ini_settings['core.binary_dir']
321 core_binary_dir = ini_settings['core.binary_dir']
320
322
321 settings.BINARY_DIR = core_binary_dir
323 settings.BINARY_DIR = core_binary_dir
322
324
323
325
324 def repo_size(ui, repo, **kwargs):
326 def repo_size(ui, repo, **kwargs):
325 extras = _extras_from_ui(ui)
327 extras = _extras_from_ui(ui)
326 return _call_hook('repo_size', extras, HgMessageWriter(ui))
328 return _call_hook('repo_size', extras, HgMessageWriter(ui))
327
329
328
330
329 def pre_pull(ui, repo, **kwargs):
331 def pre_pull(ui, repo, **kwargs):
330 extras = _extras_from_ui(ui)
332 extras = _extras_from_ui(ui)
331 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
333 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
332
334
333
335
334 def pre_pull_ssh(ui, repo, **kwargs):
336 def pre_pull_ssh(ui, repo, **kwargs):
335 extras = _extras_from_ui(ui)
337 extras = _extras_from_ui(ui)
336 if extras and extras.get('SSH'):
338 if extras and extras.get('SSH'):
337 return pre_pull(ui, repo, **kwargs)
339 return pre_pull(ui, repo, **kwargs)
338 return 0
340 return 0
339
341
340
342
341 def post_pull(ui, repo, **kwargs):
343 def post_pull(ui, repo, **kwargs):
342 extras = _extras_from_ui(ui)
344 extras = _extras_from_ui(ui)
343 return _call_hook('post_pull', extras, HgMessageWriter(ui))
345 return _call_hook('post_pull', extras, HgMessageWriter(ui))
344
346
345
347
346 def post_pull_ssh(ui, repo, **kwargs):
348 def post_pull_ssh(ui, repo, **kwargs):
347 extras = _extras_from_ui(ui)
349 extras = _extras_from_ui(ui)
348 if extras and extras.get('SSH'):
350 if extras and extras.get('SSH'):
349 return post_pull(ui, repo, **kwargs)
351 return post_pull(ui, repo, **kwargs)
350 return 0
352 return 0
351
353
352
354
353 def pre_push(ui, repo, node=None, **kwargs):
355 def pre_push(ui, repo, node=None, **kwargs):
354 """
356 """
355 Mercurial pre_push hook
357 Mercurial pre_push hook
356 """
358 """
357 extras = _extras_from_ui(ui)
359 extras = _extras_from_ui(ui)
358 detect_force_push = extras.get('detect_force_push')
360 detect_force_push = extras.get('detect_force_push')
359
361
360 rev_data = []
362 rev_data = []
361 hook_type: str = safe_str(kwargs.get('hooktype'))
363 hook_type: str = safe_str(kwargs.get('hooktype'))
362
364
363 if node and hook_type == 'pretxnchangegroup':
365 if node and hook_type == 'pretxnchangegroup':
364 branches = collections.defaultdict(list)
366 branches = collections.defaultdict(list)
365 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
367 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
366 for commit_id, branch in commits:
368 for commit_id, branch in commits:
367 branches[branch].append(commit_id)
369 branches[branch].append(commit_id)
368
370
369 for branch, commits in branches.items():
371 for branch, commits in branches.items():
370 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
372 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
371 rev_data.append({
373 rev_data.append({
372 'total_commits': len(commits),
374 'total_commits': len(commits),
373 'old_rev': old_rev,
375 'old_rev': old_rev,
374 'new_rev': commits[-1],
376 'new_rev': commits[-1],
375 'ref': '',
377 'ref': '',
376 'type': 'branch',
378 'type': 'branch',
377 'name': branch,
379 'name': branch,
378 })
380 })
379
381
380 for push_ref in rev_data:
382 for push_ref in rev_data:
381 push_ref['multiple_heads'] = _heads
383 push_ref['multiple_heads'] = _heads
382
384
383 repo_path = os.path.join(
385 repo_path = os.path.join(
384 extras.get('repo_store', ''), extras.get('repository', ''))
386 extras.get('repo_store', ''), extras.get('repository', ''))
385 push_ref['hg_env'] = _get_hg_env(
387 push_ref['hg_env'] = _get_hg_env(
386 old_rev=push_ref['old_rev'],
388 old_rev=push_ref['old_rev'],
387 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
389 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
388 repo_path=repo_path)
390 repo_path=repo_path)
389
391
390 extras['hook_type'] = hook_type or 'pre_push'
392 extras['hook_type'] = hook_type or 'pre_push'
391 extras['commit_ids'] = rev_data
393 extras['commit_ids'] = rev_data
392
394
393 return _call_hook('pre_push', extras, HgMessageWriter(ui))
395 return _call_hook('pre_push', extras, HgMessageWriter(ui))
394
396
395
397
396 def pre_push_ssh(ui, repo, node=None, **kwargs):
398 def pre_push_ssh(ui, repo, node=None, **kwargs):
397 extras = _extras_from_ui(ui)
399 extras = _extras_from_ui(ui)
398 if extras.get('SSH'):
400 if extras.get('SSH'):
399 return pre_push(ui, repo, node, **kwargs)
401 return pre_push(ui, repo, node, **kwargs)
400
402
401 return 0
403 return 0
402
404
403
405
404 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
406 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
405 """
407 """
406 Mercurial pre_push hook for SSH
408 Mercurial pre_push hook for SSH
407 """
409 """
408 extras = _extras_from_ui(ui)
410 extras = _extras_from_ui(ui)
409 if extras.get('SSH'):
411 if extras.get('SSH'):
410 permission = extras['SSH_PERMISSIONS']
412 permission = extras['SSH_PERMISSIONS']
411
413
412 if 'repository.write' == permission or 'repository.admin' == permission:
414 if 'repository.write' == permission or 'repository.admin' == permission:
413 return 0
415 return 0
414
416
415 # non-zero ret code
417 # non-zero ret code
416 return 1
418 return 1
417
419
418 return 0
420 return 0
419
421
420
422
421 def post_push(ui, repo, node, **kwargs):
423 def post_push(ui, repo, node, **kwargs):
422 """
424 """
423 Mercurial post_push hook
425 Mercurial post_push hook
424 """
426 """
425 extras = _extras_from_ui(ui)
427 extras = _extras_from_ui(ui)
426
428
427 commit_ids = []
429 commit_ids = []
428 branches = []
430 branches = []
429 bookmarks = []
431 bookmarks = []
430 tags = []
432 tags = []
431 hook_type: str = safe_str(kwargs.get('hooktype'))
433 hook_type: str = safe_str(kwargs.get('hooktype'))
432
434
433 commits, _heads = _rev_range_hash(repo, node)
435 commits, _heads = _rev_range_hash(repo, node)
434 for commit_id, branch in commits:
436 for commit_id, branch in commits:
435 commit_ids.append(commit_id)
437 commit_ids.append(commit_id)
436 if branch not in branches:
438 if branch not in branches:
437 branches.append(branch)
439 branches.append(branch)
438
440
439 if hasattr(ui, '_rc_pushkey_bookmarks'):
441 if hasattr(ui, '_rc_pushkey_bookmarks'):
440 bookmarks = ui._rc_pushkey_bookmarks
442 bookmarks = ui._rc_pushkey_bookmarks
441
443
442 extras['hook_type'] = hook_type or 'post_push'
444 extras['hook_type'] = hook_type or 'post_push'
443 extras['commit_ids'] = commit_ids
445 extras['commit_ids'] = commit_ids
444
446
445 extras['new_refs'] = {
447 extras['new_refs'] = {
446 'branches': branches,
448 'branches': branches,
447 'bookmarks': bookmarks,
449 'bookmarks': bookmarks,
448 'tags': tags
450 'tags': tags
449 }
451 }
450
452
451 return _call_hook('post_push', extras, HgMessageWriter(ui))
453 return _call_hook('post_push', extras, HgMessageWriter(ui))
452
454
453
455
454 def post_push_ssh(ui, repo, node, **kwargs):
456 def post_push_ssh(ui, repo, node, **kwargs):
455 """
457 """
456 Mercurial post_push hook for SSH
458 Mercurial post_push hook for SSH
457 """
459 """
458 if _extras_from_ui(ui).get('SSH'):
460 if _extras_from_ui(ui).get('SSH'):
459 return post_push(ui, repo, node, **kwargs)
461 return post_push(ui, repo, node, **kwargs)
460 return 0
462 return 0
461
463
462
464
463 def key_push(ui, repo, **kwargs):
465 def key_push(ui, repo, **kwargs):
464 from vcsserver.hgcompat import get_ctx
466 from vcsserver.hgcompat import get_ctx
465
467
466 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
468 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
467 # store new bookmarks in our UI object propagated later to post_push
469 # store new bookmarks in our UI object propagated later to post_push
468 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
470 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
469 return
471 return
470
472
471
473
472 # backward compat
474 # backward compat
473 log_pull_action = post_pull
475 log_pull_action = post_pull
474
476
475 # backward compat
477 # backward compat
476 log_push_action = post_push
478 log_push_action = post_push
477
479
478
480
479 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
481 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
480 """
482 """
481 Old hook name: keep here for backward compatibility.
483 Old hook name: keep here for backward compatibility.
482
484
483 This is only required when the installed git hooks are not upgraded.
485 This is only required when the installed git hooks are not upgraded.
484 """
486 """
485 pass
487 pass
486
488
487
489
488 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
490 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
489 """
491 """
490 Old hook name: keep here for backward compatibility.
492 Old hook name: keep here for backward compatibility.
491
493
492 This is only required when the installed git hooks are not upgraded.
494 This is only required when the installed git hooks are not upgraded.
493 """
495 """
494 pass
496 pass
495
497
496
498
497 @dataclasses.dataclass
499 @dataclasses.dataclass
498 class HookResponse:
500 class HookResponse:
499 status: int
501 status: int
500 output: str
502 output: str
501
503
502
504
503 def git_pre_pull(extras) -> HookResponse:
505 def git_pre_pull(extras) -> HookResponse:
504 """
506 """
505 Pre pull hook.
507 Pre pull hook.
506
508
507 :param extras: dictionary containing the keys defined in simplevcs
509 :param extras: dictionary containing the keys defined in simplevcs
508 :type extras: dict
510 :type extras: dict
509
511
510 :return: status code of the hook. 0 for success.
512 :return: status code of the hook. 0 for success.
511 :rtype: int
513 :rtype: int
512 """
514 """
513
515
514 if 'pull' not in extras['hooks']:
516 if 'pull' not in extras['hooks']:
515 return HookResponse(0, '')
517 return HookResponse(0, '')
516
518
517 stdout = io.StringIO()
519 stdout = io.StringIO()
518 try:
520 try:
519 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
521 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
520
522
521 except Exception as error:
523 except Exception as error:
522 log.exception('Failed to call pre_pull hook')
524 log.exception('Failed to call pre_pull hook')
523 status_code = 128
525 status_code = 128
524 stdout.write(f'ERROR: {error}\n')
526 stdout.write(f'ERROR: {error}\n')
525
527
526 return HookResponse(status_code, stdout.getvalue())
528 return HookResponse(status_code, stdout.getvalue())
527
529
528
530
529 def git_post_pull(extras) -> HookResponse:
531 def git_post_pull(extras) -> HookResponse:
530 """
532 """
531 Post pull hook.
533 Post pull hook.
532
534
533 :param extras: dictionary containing the keys defined in simplevcs
535 :param extras: dictionary containing the keys defined in simplevcs
534 :type extras: dict
536 :type extras: dict
535
537
536 :return: status code of the hook. 0 for success.
538 :return: status code of the hook. 0 for success.
537 :rtype: int
539 :rtype: int
538 """
540 """
539 if 'pull' not in extras['hooks']:
541 if 'pull' not in extras['hooks']:
540 return HookResponse(0, '')
542 return HookResponse(0, '')
541
543
542 stdout = io.StringIO()
544 stdout = io.StringIO()
543 try:
545 try:
544 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
546 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
545 except Exception as error:
547 except Exception as error:
546 status = 128
548 status = 128
547 stdout.write(f'ERROR: {error}\n')
549 stdout.write(f'ERROR: {error}\n')
548
550
549 return HookResponse(status, stdout.getvalue())
551 return HookResponse(status, stdout.getvalue())
550
552
551
553
552 def _parse_git_ref_lines(revision_lines):
554 def _parse_git_ref_lines(revision_lines):
553 rev_data = []
555 rev_data = []
554 for revision_line in revision_lines or []:
556 for revision_line in revision_lines or []:
555 old_rev, new_rev, ref = revision_line.strip().split(' ')
557 old_rev, new_rev, ref = revision_line.strip().split(' ')
556 ref_data = ref.split('/', 2)
558 ref_data = ref.split('/', 2)
557 if ref_data[1] in ('tags', 'heads'):
559 if ref_data[1] in ('tags', 'heads'):
558 rev_data.append({
560 rev_data.append({
559 # NOTE(marcink):
561 # NOTE(marcink):
560 # we're unable to tell total_commits for git at this point
562 # we're unable to tell total_commits for git at this point
561 # but we set the variable for consistency with GIT
563 # but we set the variable for consistency with GIT
562 'total_commits': -1,
564 'total_commits': -1,
563 'old_rev': old_rev,
565 'old_rev': old_rev,
564 'new_rev': new_rev,
566 'new_rev': new_rev,
565 'ref': ref,
567 'ref': ref,
566 'type': ref_data[1],
568 'type': ref_data[1],
567 'name': ref_data[2],
569 'name': ref_data[2],
568 })
570 })
569 return rev_data
571 return rev_data
570
572
571
573
572 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
574 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
573 """
575 """
574 Pre push hook.
576 Pre push hook.
575
577
576 :return: status code of the hook. 0 for success.
578 :return: status code of the hook. 0 for success.
577 """
579 """
578 extras = json.loads(env['RC_SCM_DATA'])
580 extras = json.loads(env['RC_SCM_DATA'])
579 rev_data = _parse_git_ref_lines(revision_lines)
581 rev_data = _parse_git_ref_lines(revision_lines)
580 if 'push' not in extras['hooks']:
582 if 'push' not in extras['hooks']:
581 return 0
583 return 0
582 _fix_hooks_executables(env.get('RC_INI_FILE'))
584 _fix_hooks_executables(env.get('RC_INI_FILE'))
583
585
584 empty_commit_id = '0' * 40
586 empty_commit_id = '0' * 40
585
587
586 detect_force_push = extras.get('detect_force_push')
588 detect_force_push = extras.get('detect_force_push')
587
589
588 for push_ref in rev_data:
590 for push_ref in rev_data:
589 # store our git-env which holds the temp store
591 # store our git-env which holds the temp store
590 push_ref['git_env'] = _get_git_env()
592 push_ref['git_env'] = _get_git_env()
591 push_ref['pruned_sha'] = ''
593 push_ref['pruned_sha'] = ''
592 if not detect_force_push:
594 if not detect_force_push:
593 # don't check for forced-push when we don't need to
595 # don't check for forced-push when we don't need to
594 continue
596 continue
595
597
596 type_ = push_ref['type']
598 type_ = push_ref['type']
597 new_branch = push_ref['old_rev'] == empty_commit_id
599 new_branch = push_ref['old_rev'] == empty_commit_id
598 delete_branch = push_ref['new_rev'] == empty_commit_id
600 delete_branch = push_ref['new_rev'] == empty_commit_id
599 if type_ == 'heads' and not (new_branch or delete_branch):
601 if type_ == 'heads' and not (new_branch or delete_branch):
600 old_rev = push_ref['old_rev']
602 old_rev = push_ref['old_rev']
601 new_rev = push_ref['new_rev']
603 new_rev = push_ref['new_rev']
602 cmd = [settings.GIT_EXECUTABLE(), 'rev-list', old_rev, f'^{new_rev}']
604 cmd = [settings.GIT_EXECUTABLE(), 'rev-list', old_rev, f'^{new_rev}']
603 stdout, stderr = subprocessio.run_command(
605 stdout, stderr = subprocessio.run_command(
604 cmd, env=os.environ.copy())
606 cmd, env=os.environ.copy())
605 # means we're having some non-reachable objects, this forced push was used
607 # means we're having some non-reachable objects, this forced push was used
606 if stdout:
608 if stdout:
607 push_ref['pruned_sha'] = stdout.splitlines()
609 push_ref['pruned_sha'] = stdout.splitlines()
608
610
609 extras['hook_type'] = 'pre_receive'
611 extras['hook_type'] = 'pre_receive'
610 extras['commit_ids'] = rev_data
612 extras['commit_ids'] = rev_data
611
613
612 stdout = sys.stdout
614 stdout = sys.stdout
613 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
615 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
614
616
615 return status_code
617 return status_code
616
618
617
619
618 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
620 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
619 """
621 """
620 Post push hook.
622 Post push hook.
621
623
622 :return: status code of the hook. 0 for success.
624 :return: status code of the hook. 0 for success.
623 """
625 """
624 extras = json.loads(env['RC_SCM_DATA'])
626 extras = json.loads(env['RC_SCM_DATA'])
625 if 'push' not in extras['hooks']:
627 if 'push' not in extras['hooks']:
626 return 0
628 return 0
627
629
628 _fix_hooks_executables(env.get('RC_INI_FILE'))
630 _fix_hooks_executables(env.get('RC_INI_FILE'))
629
631
630 rev_data = _parse_git_ref_lines(revision_lines)
632 rev_data = _parse_git_ref_lines(revision_lines)
631
633
632 git_revs = []
634 git_revs = []
633
635
634 # N.B.(skreft): it is ok to just call git, as git before calling a
636 # N.B.(skreft): it is ok to just call git, as git before calling a
635 # subcommand sets the PATH environment variable so that it point to the
637 # subcommand sets the PATH environment variable so that it point to the
636 # correct version of the git executable.
638 # correct version of the git executable.
637 empty_commit_id = '0' * 40
639 empty_commit_id = '0' * 40
638 branches = []
640 branches = []
639 tags = []
641 tags = []
640 for push_ref in rev_data:
642 for push_ref in rev_data:
641 type_ = push_ref['type']
643 type_ = push_ref['type']
642
644
643 if type_ == 'heads':
645 if type_ == 'heads':
644 # starting new branch case
646 # starting new branch case
645 if push_ref['old_rev'] == empty_commit_id:
647 if push_ref['old_rev'] == empty_commit_id:
646 push_ref_name = push_ref['name']
648 push_ref_name = push_ref['name']
647
649
648 if push_ref_name not in branches:
650 if push_ref_name not in branches:
649 branches.append(push_ref_name)
651 branches.append(push_ref_name)
650
652
651 need_head_set = ''
653 need_head_set = ''
652 with Repository(os.getcwd()) as repo:
654 with Repository(os.getcwd()) as repo:
653 try:
655 try:
654 repo.head
656 repo.head
655 except pygit2.GitError:
657 except pygit2.GitError:
656 need_head_set = f'refs/heads/{push_ref_name}'
658 need_head_set = f'refs/heads/{push_ref_name}'
657
659
658 if need_head_set:
660 if need_head_set:
659 repo.set_head(need_head_set)
661 repo.set_head(need_head_set)
660 print(f"Setting default branch to {push_ref_name}")
662 print(f"Setting default branch to {push_ref_name}")
661
663
662 cmd = [settings.GIT_EXECUTABLE(), 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
664 cmd = [settings.GIT_EXECUTABLE(), 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
663 stdout, stderr = subprocessio.run_command(
665 stdout, stderr = subprocessio.run_command(
664 cmd, env=os.environ.copy())
666 cmd, env=os.environ.copy())
665 heads = safe_str(stdout)
667 heads = safe_str(stdout)
666 heads = heads.replace(push_ref['ref'], '')
668 heads = heads.replace(push_ref['ref'], '')
667 heads = ' '.join(head for head
669 heads = ' '.join(head for head
668 in heads.splitlines() if head) or '.'
670 in heads.splitlines() if head) or '.'
669 cmd = [settings.GIT_EXECUTABLE(), 'log', '--reverse',
671 cmd = [settings.GIT_EXECUTABLE(), 'log', '--reverse',
670 '--pretty=format:%H', '--', push_ref['new_rev'],
672 '--pretty=format:%H', '--', push_ref['new_rev'],
671 '--not', heads]
673 '--not', heads]
672 stdout, stderr = subprocessio.run_command(
674 stdout, stderr = subprocessio.run_command(
673 cmd, env=os.environ.copy())
675 cmd, env=os.environ.copy())
674 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
676 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
675
677
676 # delete branch case
678 # delete branch case
677 elif push_ref['new_rev'] == empty_commit_id:
679 elif push_ref['new_rev'] == empty_commit_id:
678 git_revs.append(f'delete_branch=>{push_ref["name"]}')
680 git_revs.append(f'delete_branch=>{push_ref["name"]}')
679 else:
681 else:
680 if push_ref['name'] not in branches:
682 if push_ref['name'] not in branches:
681 branches.append(push_ref['name'])
683 branches.append(push_ref['name'])
682
684
683 cmd = [settings.GIT_EXECUTABLE(), 'log',
685 cmd = [settings.GIT_EXECUTABLE(), 'log',
684 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
686 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
685 '--reverse', '--pretty=format:%H']
687 '--reverse', '--pretty=format:%H']
686 stdout, stderr = subprocessio.run_command(
688 stdout, stderr = subprocessio.run_command(
687 cmd, env=os.environ.copy())
689 cmd, env=os.environ.copy())
688 # we get bytes from stdout, we need str to be consistent
690 # we get bytes from stdout, we need str to be consistent
689 log_revs = list(map(ascii_str, stdout.splitlines()))
691 log_revs = list(map(ascii_str, stdout.splitlines()))
690 git_revs.extend(log_revs)
692 git_revs.extend(log_revs)
691
693
692 # Pure pygit2 impl. but still 2-3x slower :/
694 # Pure pygit2 impl. but still 2-3x slower :/
693 # results = []
695 # results = []
694 #
696 #
695 # with Repository(os.getcwd()) as repo:
697 # with Repository(os.getcwd()) as repo:
696 # repo_new_rev = repo[push_ref['new_rev']]
698 # repo_new_rev = repo[push_ref['new_rev']]
697 # repo_old_rev = repo[push_ref['old_rev']]
699 # repo_old_rev = repo[push_ref['old_rev']]
698 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
700 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
699 #
701 #
700 # for commit in walker:
702 # for commit in walker:
701 # if commit.id == repo_old_rev.id:
703 # if commit.id == repo_old_rev.id:
702 # break
704 # break
703 # results.append(commit.id.hex)
705 # results.append(commit.id.hex)
704 # # reverse the order, can't use GIT_SORT_REVERSE
706 # # reverse the order, can't use GIT_SORT_REVERSE
705 # log_revs = results[::-1]
707 # log_revs = results[::-1]
706
708
707 elif type_ == 'tags':
709 elif type_ == 'tags':
708 if push_ref['name'] not in tags:
710 if push_ref['name'] not in tags:
709 tags.append(push_ref['name'])
711 tags.append(push_ref['name'])
710 git_revs.append(f'tag=>{push_ref["name"]}')
712 git_revs.append(f'tag=>{push_ref["name"]}')
711
713
712 extras['hook_type'] = 'post_receive'
714 extras['hook_type'] = 'post_receive'
713 extras['commit_ids'] = git_revs
715 extras['commit_ids'] = git_revs
714 extras['new_refs'] = {
716 extras['new_refs'] = {
715 'branches': branches,
717 'branches': branches,
716 'bookmarks': [],
718 'bookmarks': [],
717 'tags': tags,
719 'tags': tags,
718 }
720 }
719
721
720 stdout = sys.stdout
722 stdout = sys.stdout
721
723
722 if 'repo_size' in extras['hooks']:
724 if 'repo_size' in extras['hooks']:
723 try:
725 try:
724 _call_hook('repo_size', extras, GitMessageWriter(stdout))
726 _call_hook('repo_size', extras, GitMessageWriter(stdout))
725 except Exception:
727 except Exception:
726 pass
728 pass
727
729
728 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
730 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
729 return status_code
731 return status_code
730
732
731
733
732 def get_extras_from_txn_id(repo_path, txn_id):
734 def get_extras_from_txn_id(repo_path, txn_id):
733 extras = get_txn_id_from_store(repo_path, txn_id)
735 extras = get_txn_id_from_store(repo_path, txn_id)
734 return extras
736 return extras
735
737
736
738
737 def svn_pre_commit(repo_path, commit_data, env):
739 def svn_pre_commit(repo_path, commit_data, env):
738
740
739 path, txn_id = commit_data
741 path, txn_id = commit_data
740 branches = []
742 branches = []
741 tags = []
743 tags = []
742
744
743 if env.get('RC_SCM_DATA'):
745 if env.get('RC_SCM_DATA'):
744 extras = json.loads(env['RC_SCM_DATA'])
746 extras = json.loads(env['RC_SCM_DATA'])
745 else:
747 else:
746 ini_path = env.get('RC_INI_FILE')
748 ini_path = env.get('RC_INI_FILE')
747 if ini_path:
749 if ini_path:
748 _get_ini_settings(ini_path)
750 _get_ini_settings(ini_path)
749 # fallback method to read from TXN-ID stored data
751 # fallback method to read from TXN-ID stored data
750 extras = get_extras_from_txn_id(path, txn_id)
752 extras = get_extras_from_txn_id(path, txn_id)
751
753
752 if not extras:
754 if not extras:
753 raise ValueError('SVN-PRE-COMMIT: Failed to extract context data in called extras for hook execution')
755 raise ValueError('SVN-PRE-COMMIT: Failed to extract context data in called extras for hook execution')
754
756
755 if extras.get('rc_internal_commit'):
757 if extras.get('rc_internal_commit'):
756 # special marker for internal commit, we don't call hooks client
758 # special marker for internal commit, we don't call hooks client
757 return 0
759 return 0
758
760
759 extras['hook_type'] = 'pre_commit'
761 extras['hook_type'] = 'pre_commit'
760 extras['commit_ids'] = [txn_id]
762 extras['commit_ids'] = [txn_id]
761 extras['txn_id'] = txn_id
763 extras['txn_id'] = txn_id
762 extras['new_refs'] = {
764 extras['new_refs'] = {
763 'total_commits': 1,
765 'total_commits': 1,
764 'branches': branches,
766 'branches': branches,
765 'bookmarks': [],
767 'bookmarks': [],
766 'tags': tags,
768 'tags': tags,
767 }
769 }
768
770
769 return _call_hook('pre_push', extras, SvnMessageWriter())
771 return _call_hook('pre_push', extras, SvnMessageWriter())
770
772
771
773
772 def svn_post_commit(repo_path, commit_data, env):
774 def svn_post_commit(repo_path, commit_data, env):
773 """
775 """
774 commit_data is path, rev, txn_id
776 commit_data is path, rev, txn_id
775 """
777 """
776
778
777 if len(commit_data) == 3:
779 if len(commit_data) == 3:
778 path, commit_id, txn_id = commit_data
780 path, commit_id, txn_id = commit_data
779 elif len(commit_data) == 2:
781 elif len(commit_data) == 2:
780 log.error('Failed to extract txn_id from commit_data using legacy method. '
782 log.error('Failed to extract txn_id from commit_data using legacy method. '
781 'Some functionality might be limited')
783 'Some functionality might be limited')
782 path, commit_id = commit_data
784 path, commit_id = commit_data
783 txn_id = None
785 txn_id = None
784 else:
786 else:
785 return 0
787 return 0
786
788
787 branches = []
789 branches = []
788 tags = []
790 tags = []
789
791
790 if env.get('RC_SCM_DATA'):
792 if env.get('RC_SCM_DATA'):
791 extras = json.loads(env['RC_SCM_DATA'])
793 extras = json.loads(env['RC_SCM_DATA'])
792 else:
794 else:
793 ini_path = env.get('RC_INI_FILE')
795 ini_path = env.get('RC_INI_FILE')
794 if ini_path:
796 if ini_path:
795 _get_ini_settings(ini_path)
797 _get_ini_settings(ini_path)
796 # fallback method to read from TXN-ID stored data
798 # fallback method to read from TXN-ID stored data
797 extras = get_extras_from_txn_id(path, txn_id)
799 extras = get_extras_from_txn_id(path, txn_id)
798
800
799 if not extras and txn_id:
801 if not extras and txn_id:
800 raise ValueError('SVN-POST-COMMIT: Failed to extract context data in called extras for hook execution')
802 raise ValueError('SVN-POST-COMMIT: Failed to extract context data in called extras for hook execution')
801
803
802 if extras.get('rc_internal_commit'):
804 if extras.get('rc_internal_commit'):
803 # special marker for internal commit, we don't call hooks client
805 # special marker for internal commit, we don't call hooks client
804 return 0
806 return 0
805
807
806 extras['hook_type'] = 'post_commit'
808 extras['hook_type'] = 'post_commit'
807 extras['commit_ids'] = [commit_id]
809 extras['commit_ids'] = [commit_id]
808 extras['txn_id'] = txn_id
810 extras['txn_id'] = txn_id
809 extras['new_refs'] = {
811 extras['new_refs'] = {
810 'branches': branches,
812 'branches': branches,
811 'bookmarks': [],
813 'bookmarks': [],
812 'tags': tags,
814 'tags': tags,
813 'total_commits': 1,
815 'total_commits': 1,
814 }
816 }
815
817
816 if 'repo_size' in extras['hooks']:
818 if 'repo_size' in extras['hooks']:
817 try:
819 try:
818 _call_hook('repo_size', extras, SvnMessageWriter())
820 _call_hook('repo_size', extras, SvnMessageWriter())
819 except Exception:
821 except Exception:
820 pass
822 pass
821
823
822 return _call_hook('post_push', extras, SvnMessageWriter())
824 return _call_hook('post_push', extras, SvnMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now