##// END OF EJS Templates
hooks: handle non-ascii characters in hooks new pull-requests open template.
marcink -
r3331:540b9430 default
parent child Browse files
Show More
@@ -1,493 +1,492 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of hooks run by RhodeCode Enterprise
24 24 """
25 25
26 26 import os
27 27 import collections
28 28 import logging
29 29
30 30 import rhodecode
31 31 from rhodecode import events
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib.utils2 import safe_str
35 35 from rhodecode.lib.exceptions import (
36 36 HTTPLockedRC, HTTPBranchProtected, UserCreationError)
37 37 from rhodecode.model.db import Repository, User
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class HookResponse(object):
43 43 def __init__(self, status, output):
44 44 self.status = status
45 45 self.output = output
46 46
47 47 def __add__(self, other):
48 48 other_status = getattr(other, 'status', 0)
49 49 new_status = max(self.status, other_status)
50 50 other_output = getattr(other, 'output', '')
51 51 new_output = self.output + other_output
52 52
53 53 return HookResponse(new_status, new_output)
54 54
55 55 def __bool__(self):
56 56 return self.status == 0
57 57
58 58
59 59 def is_shadow_repo(extras):
60 60 """
61 61 Returns ``True`` if this is an action executed against a shadow repository.
62 62 """
63 63 return extras['is_shadow_repo']
64 64
65 65
66 66 def _get_scm_size(alias, root_path):
67 67
68 68 if not alias.startswith('.'):
69 69 alias += '.'
70 70
71 71 size_scm, size_root = 0, 0
72 72 for path, unused_dirs, files in os.walk(safe_str(root_path)):
73 73 if path.find(alias) != -1:
74 74 for f in files:
75 75 try:
76 76 size_scm += os.path.getsize(os.path.join(path, f))
77 77 except OSError:
78 78 pass
79 79 else:
80 80 for f in files:
81 81 try:
82 82 size_root += os.path.getsize(os.path.join(path, f))
83 83 except OSError:
84 84 pass
85 85
86 86 size_scm_f = h.format_byte_size_binary(size_scm)
87 87 size_root_f = h.format_byte_size_binary(size_root)
88 88 size_total_f = h.format_byte_size_binary(size_root + size_scm)
89 89
90 90 return size_scm_f, size_root_f, size_total_f
91 91
92 92
93 93 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
94 94 def repo_size(extras):
95 95 """Present size of repository after push."""
96 96 repo = Repository.get_by_repo_name(extras.repository)
97 97 vcs_part = safe_str(u'.%s' % repo.repo_type)
98 98 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
99 99 repo.repo_full_path)
100 100 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
101 101 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
102 102 return HookResponse(0, msg)
103 103
104 104
105 105 def pre_push(extras):
106 106 """
107 107 Hook executed before pushing code.
108 108
109 109 It bans pushing when the repository is locked.
110 110 """
111 111
112 112 user = User.get_by_username(extras.username)
113 113 output = ''
114 114 if extras.locked_by[0] and user.user_id != int(extras.locked_by[0]):
115 115 locked_by = User.get(extras.locked_by[0]).username
116 116 reason = extras.locked_by[2]
117 117 # this exception is interpreted in git/hg middlewares and based
118 118 # on that proper return code is server to client
119 119 _http_ret = HTTPLockedRC(
120 120 _locked_by_explanation(extras.repository, locked_by, reason))
121 121 if str(_http_ret.code).startswith('2'):
122 122 # 2xx Codes don't raise exceptions
123 123 output = _http_ret.title
124 124 else:
125 125 raise _http_ret
126 126
127 127 hook_response = ''
128 128 if not is_shadow_repo(extras):
129 129 if extras.commit_ids and extras.check_branch_perms:
130 130
131 131 auth_user = user.AuthUser()
132 132 repo = Repository.get_by_repo_name(extras.repository)
133 133 affected_branches = []
134 134 if repo.repo_type == 'hg':
135 135 for entry in extras.commit_ids:
136 136 if entry['type'] == 'branch':
137 137 is_forced = bool(entry['multiple_heads'])
138 138 affected_branches.append([entry['name'], is_forced])
139 139 elif repo.repo_type == 'git':
140 140 for entry in extras.commit_ids:
141 141 if entry['type'] == 'heads':
142 142 is_forced = bool(entry['pruned_sha'])
143 143 affected_branches.append([entry['name'], is_forced])
144 144
145 145 for branch_name, is_forced in affected_branches:
146 146
147 147 rule, branch_perm = auth_user.get_rule_and_branch_permission(
148 148 extras.repository, branch_name)
149 149 if not branch_perm:
150 150 # no branch permission found for this branch, just keep checking
151 151 continue
152 152
153 153 if branch_perm == 'branch.push_force':
154 154 continue
155 155 elif branch_perm == 'branch.push' and is_forced is False:
156 156 continue
157 157 elif branch_perm == 'branch.push' and is_forced is True:
158 158 halt_message = 'Branch `{}` changes rejected by rule {}. ' \
159 159 'FORCE PUSH FORBIDDEN.'.format(branch_name, rule)
160 160 else:
161 161 halt_message = 'Branch `{}` changes rejected by rule {}.'.format(
162 162 branch_name, rule)
163 163
164 164 if halt_message:
165 165 _http_ret = HTTPBranchProtected(halt_message)
166 166 raise _http_ret
167 167
168 168 # Propagate to external components. This is done after checking the
169 169 # lock, for consistent behavior.
170 170 hook_response = pre_push_extension(
171 171 repo_store_path=Repository.base_path(), **extras)
172 172 events.trigger(events.RepoPrePushEvent(
173 173 repo_name=extras.repository, extras=extras))
174 174
175 175 return HookResponse(0, output) + hook_response
176 176
177 177
178 178 def pre_pull(extras):
179 179 """
180 180 Hook executed before pulling the code.
181 181
182 182 It bans pulling when the repository is locked.
183 183 """
184 184
185 185 output = ''
186 186 if extras.locked_by[0]:
187 187 locked_by = User.get(extras.locked_by[0]).username
188 188 reason = extras.locked_by[2]
189 189 # this exception is interpreted in git/hg middlewares and based
190 190 # on that proper return code is server to client
191 191 _http_ret = HTTPLockedRC(
192 192 _locked_by_explanation(extras.repository, locked_by, reason))
193 193 if str(_http_ret.code).startswith('2'):
194 194 # 2xx Codes don't raise exceptions
195 195 output = _http_ret.title
196 196 else:
197 197 raise _http_ret
198 198
199 199 # Propagate to external components. This is done after checking the
200 200 # lock, for consistent behavior.
201 201 hook_response = ''
202 202 if not is_shadow_repo(extras):
203 203 extras.hook_type = extras.hook_type or 'pre_pull'
204 204 hook_response = pre_pull_extension(
205 205 repo_store_path=Repository.base_path(), **extras)
206 206 events.trigger(events.RepoPrePullEvent(
207 207 repo_name=extras.repository, extras=extras))
208 208
209 209 return HookResponse(0, output) + hook_response
210 210
211 211
212 212 def post_pull(extras):
213 213 """Hook executed after client pulls the code."""
214 214
215 215 audit_user = audit_logger.UserWrap(
216 216 username=extras.username,
217 217 ip_addr=extras.ip)
218 218 repo = audit_logger.RepoWrap(repo_name=extras.repository)
219 219 audit_logger.store(
220 220 'user.pull', action_data={'user_agent': extras.user_agent},
221 221 user=audit_user, repo=repo, commit=True)
222 222
223 223 output = ''
224 224 # make lock is a tri state False, True, None. We only make lock on True
225 225 if extras.make_lock is True and not is_shadow_repo(extras):
226 226 user = User.get_by_username(extras.username)
227 227 Repository.lock(Repository.get_by_repo_name(extras.repository),
228 228 user.user_id,
229 229 lock_reason=Repository.LOCK_PULL)
230 230 msg = 'Made lock on repo `%s`' % (extras.repository,)
231 231 output += msg
232 232
233 233 if extras.locked_by[0]:
234 234 locked_by = User.get(extras.locked_by[0]).username
235 235 reason = extras.locked_by[2]
236 236 _http_ret = HTTPLockedRC(
237 237 _locked_by_explanation(extras.repository, locked_by, reason))
238 238 if str(_http_ret.code).startswith('2'):
239 239 # 2xx Codes don't raise exceptions
240 240 output += _http_ret.title
241 241
242 242 # Propagate to external components.
243 243 hook_response = ''
244 244 if not is_shadow_repo(extras):
245 245 extras.hook_type = extras.hook_type or 'post_pull'
246 246 hook_response = post_pull_extension(
247 247 repo_store_path=Repository.base_path(), **extras)
248 248 events.trigger(events.RepoPullEvent(
249 249 repo_name=extras.repository, extras=extras))
250 250
251 251 return HookResponse(0, output) + hook_response
252 252
253 253
254 254 def post_push(extras):
255 255 """Hook executed after user pushes to the repository."""
256 256 commit_ids = extras.commit_ids
257 257
258 258 # log the push call
259 259 audit_user = audit_logger.UserWrap(
260 260 username=extras.username, ip_addr=extras.ip)
261 261 repo = audit_logger.RepoWrap(repo_name=extras.repository)
262 262 audit_logger.store(
263 263 'user.push', action_data={
264 264 'user_agent': extras.user_agent,
265 265 'commit_ids': commit_ids[:400]},
266 266 user=audit_user, repo=repo, commit=True)
267 267
268 268 # Propagate to external components.
269 269 output = ''
270 270 # make lock is a tri state False, True, None. We only release lock on False
271 271 if extras.make_lock is False and not is_shadow_repo(extras):
272 272 Repository.unlock(Repository.get_by_repo_name(extras.repository))
273 273 msg = 'Released lock on repo `%s`\n' % extras.repository
274 274 output += msg
275 275
276 276 if extras.locked_by[0]:
277 277 locked_by = User.get(extras.locked_by[0]).username
278 278 reason = extras.locked_by[2]
279 279 _http_ret = HTTPLockedRC(
280 280 _locked_by_explanation(extras.repository, locked_by, reason))
281 281 # TODO: johbo: if not?
282 282 if str(_http_ret.code).startswith('2'):
283 283 # 2xx Codes don't raise exceptions
284 284 output += _http_ret.title
285 285
286 286 if extras.new_refs:
287 tmpl = \
288 extras.server_url + '/' + \
289 extras.repository + \
290 "/pull-request/new?{ref_type}={ref_name}"
287 tmpl = extras.server_url + '/' + extras.repository + \
288 "/pull-request/new?{ref_type}={ref_name}"
289
291 290 for branch_name in extras.new_refs['branches']:
292 291 output += 'RhodeCode: open pull request link: {}\n'.format(
293 tmpl.format(ref_type='branch', ref_name=branch_name))
292 tmpl.format(ref_type='branch', ref_name=safe_str(branch_name)))
294 293
295 294 for book_name in extras.new_refs['bookmarks']:
296 295 output += 'RhodeCode: open pull request link: {}\n'.format(
297 tmpl.format(ref_type='bookmark', ref_name=book_name))
296 tmpl.format(ref_type='bookmark', ref_name=safe_str(book_name)))
298 297
299 298 hook_response = ''
300 299 if not is_shadow_repo(extras):
301 300 hook_response = post_push_extension(
302 301 repo_store_path=Repository.base_path(),
303 302 **extras)
304 303 events.trigger(events.RepoPushEvent(
305 304 repo_name=extras.repository, pushed_commit_ids=commit_ids, extras=extras))
306 305
307 306 output += 'RhodeCode: push completed\n'
308 307 return HookResponse(0, output) + hook_response
309 308
310 309
311 310 def _locked_by_explanation(repo_name, user_name, reason):
312 311 message = (
313 312 'Repository `%s` locked by user `%s`. Reason:`%s`'
314 313 % (repo_name, user_name, reason))
315 314 return message
316 315
317 316
318 317 def check_allowed_create_user(user_dict, created_by, **kwargs):
319 318 # pre create hooks
320 319 if pre_create_user.is_active():
321 320 hook_result = pre_create_user(created_by=created_by, **user_dict)
322 321 allowed = hook_result.status == 0
323 322 if not allowed:
324 323 reason = hook_result.output
325 324 raise UserCreationError(reason)
326 325
327 326
328 327 class ExtensionCallback(object):
329 328 """
330 329 Forwards a given call to rcextensions, sanitizes keyword arguments.
331 330
332 331 Does check if there is an extension active for that hook. If it is
333 332 there, it will forward all `kwargs_keys` keyword arguments to the
334 333 extension callback.
335 334 """
336 335
337 336 def __init__(self, hook_name, kwargs_keys):
338 337 self._hook_name = hook_name
339 338 self._kwargs_keys = set(kwargs_keys)
340 339
341 340 def __call__(self, *args, **kwargs):
342 341 log.debug('Calling extension callback for `%s`', self._hook_name)
343 342 callback = self._get_callback()
344 343 if not callback:
345 344 log.debug('extension callback `%s` not found, skipping...', self._hook_name)
346 345 return
347 346
348 347 kwargs_to_pass = {}
349 348 for key in self._kwargs_keys:
350 349 try:
351 350 kwargs_to_pass[key] = kwargs[key]
352 351 except KeyError:
353 352 log.error('Failed to fetch %s key. Expected keys: %s',
354 353 key, self._kwargs_keys)
355 354 raise
356 355
357 356 # backward compat for removed api_key for old hooks. This was it works
358 357 # with older rcextensions that require api_key present
359 358 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
360 359 kwargs_to_pass['api_key'] = '_DEPRECATED_'
361 360 return callback(**kwargs_to_pass)
362 361
363 362 def is_active(self):
364 363 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
365 364
366 365 def _get_callback(self):
367 366 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
368 367
369 368
370 369 pre_pull_extension = ExtensionCallback(
371 370 hook_name='PRE_PULL_HOOK',
372 371 kwargs_keys=(
373 372 'server_url', 'config', 'scm', 'username', 'ip', 'action',
374 373 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
375 374
376 375
377 376 post_pull_extension = ExtensionCallback(
378 377 hook_name='PULL_HOOK',
379 378 kwargs_keys=(
380 379 'server_url', 'config', 'scm', 'username', 'ip', 'action',
381 380 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
382 381
383 382
384 383 pre_push_extension = ExtensionCallback(
385 384 hook_name='PRE_PUSH_HOOK',
386 385 kwargs_keys=(
387 386 'server_url', 'config', 'scm', 'username', 'ip', 'action',
388 387 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
389 388
390 389
391 390 post_push_extension = ExtensionCallback(
392 391 hook_name='PUSH_HOOK',
393 392 kwargs_keys=(
394 393 'server_url', 'config', 'scm', 'username', 'ip', 'action',
395 394 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
396 395
397 396
398 397 pre_create_user = ExtensionCallback(
399 398 hook_name='PRE_CREATE_USER_HOOK',
400 399 kwargs_keys=(
401 400 'username', 'password', 'email', 'firstname', 'lastname', 'active',
402 401 'admin', 'created_by'))
403 402
404 403
405 404 log_create_pull_request = ExtensionCallback(
406 405 hook_name='CREATE_PULL_REQUEST',
407 406 kwargs_keys=(
408 407 'server_url', 'config', 'scm', 'username', 'ip', 'action',
409 408 'repository', 'pull_request_id', 'url', 'title', 'description',
410 409 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
411 410 'mergeable', 'source', 'target', 'author', 'reviewers'))
412 411
413 412
414 413 log_merge_pull_request = ExtensionCallback(
415 414 hook_name='MERGE_PULL_REQUEST',
416 415 kwargs_keys=(
417 416 'server_url', 'config', 'scm', 'username', 'ip', 'action',
418 417 'repository', 'pull_request_id', 'url', 'title', 'description',
419 418 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
420 419 'mergeable', 'source', 'target', 'author', 'reviewers'))
421 420
422 421
423 422 log_close_pull_request = ExtensionCallback(
424 423 hook_name='CLOSE_PULL_REQUEST',
425 424 kwargs_keys=(
426 425 'server_url', 'config', 'scm', 'username', 'ip', 'action',
427 426 'repository', 'pull_request_id', 'url', 'title', 'description',
428 427 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
429 428 'mergeable', 'source', 'target', 'author', 'reviewers'))
430 429
431 430
432 431 log_review_pull_request = ExtensionCallback(
433 432 hook_name='REVIEW_PULL_REQUEST',
434 433 kwargs_keys=(
435 434 'server_url', 'config', 'scm', 'username', 'ip', 'action',
436 435 'repository', 'pull_request_id', 'url', 'title', 'description',
437 436 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
438 437 'mergeable', 'source', 'target', 'author', 'reviewers'))
439 438
440 439
441 440 log_update_pull_request = ExtensionCallback(
442 441 hook_name='UPDATE_PULL_REQUEST',
443 442 kwargs_keys=(
444 443 'server_url', 'config', 'scm', 'username', 'ip', 'action',
445 444 'repository', 'pull_request_id', 'url', 'title', 'description',
446 445 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
447 446 'mergeable', 'source', 'target', 'author', 'reviewers'))
448 447
449 448
450 449 log_create_user = ExtensionCallback(
451 450 hook_name='CREATE_USER_HOOK',
452 451 kwargs_keys=(
453 452 'username', 'full_name_or_username', 'full_contact', 'user_id',
454 453 'name', 'firstname', 'short_contact', 'admin', 'lastname',
455 454 'ip_addresses', 'extern_type', 'extern_name',
456 455 'email', 'api_keys', 'last_login',
457 456 'full_name', 'active', 'password', 'emails',
458 457 'inherit_default_permissions', 'created_by', 'created_on'))
459 458
460 459
461 460 log_delete_user = ExtensionCallback(
462 461 hook_name='DELETE_USER_HOOK',
463 462 kwargs_keys=(
464 463 'username', 'full_name_or_username', 'full_contact', 'user_id',
465 464 'name', 'firstname', 'short_contact', 'admin', 'lastname',
466 465 'ip_addresses',
467 466 'email', 'last_login',
468 467 'full_name', 'active', 'password', 'emails',
469 468 'inherit_default_permissions', 'deleted_by'))
470 469
471 470
472 471 log_create_repository = ExtensionCallback(
473 472 hook_name='CREATE_REPO_HOOK',
474 473 kwargs_keys=(
475 474 'repo_name', 'repo_type', 'description', 'private', 'created_on',
476 475 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
477 476 'clone_uri', 'fork_id', 'group_id', 'created_by'))
478 477
479 478
480 479 log_delete_repository = ExtensionCallback(
481 480 hook_name='DELETE_REPO_HOOK',
482 481 kwargs_keys=(
483 482 'repo_name', 'repo_type', 'description', 'private', 'created_on',
484 483 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
485 484 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
486 485
487 486
488 487 log_create_repository_group = ExtensionCallback(
489 488 hook_name='CREATE_REPO_GROUP_HOOK',
490 489 kwargs_keys=(
491 490 'group_name', 'group_parent_id', 'group_description',
492 491 'group_id', 'user_id', 'created_by', 'created_on',
493 492 'enable_locking'))
General Comments 0
You need to be logged in to leave comments. Login now