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