Show More
@@ -0,0 +1,41 b'' | |||
|
1 | .. _locking: | |
|
2 | ||
|
3 | =================================== | |
|
4 | RhodeCode repository locking system | |
|
5 | =================================== | |
|
6 | ||
|
7 | ||
|
8 | | Repos with **locking function=disabled** is the default, that's how repos work | |
|
9 | today. | |
|
10 | | Repos with **locking function=enabled** behaves like follows: | |
|
11 | ||
|
12 | Repos have a state called `locked` that can be true or false. | |
|
13 | The hg/git commands `hg/git clone`, `hg/git pull`, and `hg/git push` | |
|
14 | influence this state: | |
|
15 | ||
|
16 | - The command `hg/git pull <repo>` will lock that repo (locked=true) | |
|
17 | if the user has write/admin permissions on this repo | |
|
18 | ||
|
19 | - The command `hg/git clone <repo>` will lock that repo (locked=true) if the | |
|
20 | user has write/admin permissions on this repo | |
|
21 | ||
|
22 | ||
|
23 | RhodeCode will remember the user id who locked the repo | |
|
24 | only this specific user can unlock the repo (locked=false) by calling | |
|
25 | ||
|
26 | - `hg/git push <repo>` | |
|
27 | ||
|
28 | every other command on that repo from this user and | |
|
29 | every command from any other user will result in http return code 423 (locked) | |
|
30 | ||
|
31 | ||
|
32 | additionally the http error includes the <user> that locked the repo | |
|
33 | (e.g. “repository <repo> locked by user <user>”) | |
|
34 | ||
|
35 | ||
|
36 | So the scenario of use for repos with `locking function` enabled is that | |
|
37 | every initial clone and every pull gives users (with write permission) | |
|
38 | the exclusive right to do a push. | |
|
39 | ||
|
40 | ||
|
41 | Each repo can be manually unlocked by admin from the repo settings menu. No newline at end of file |
@@ -0,0 +1,31 b'' | |||
|
1 | #!/usr/bin/env python | |
|
2 | import os | |
|
3 | import sys | |
|
4 | ||
|
5 | try: | |
|
6 | import rhodecode | |
|
7 | RC_HOOK_VER = '_TMPL_' | |
|
8 | os.environ['RC_HOOK_VER'] = RC_HOOK_VER | |
|
9 | from rhodecode.lib.hooks import handle_git_pre_receive | |
|
10 | except ImportError: | |
|
11 | rhodecode = None | |
|
12 | ||
|
13 | ||
|
14 | def main(): | |
|
15 | if rhodecode is None: | |
|
16 | # exit with success if we cannot import rhodecode !! | |
|
17 | # this allows simply push to this repo even without | |
|
18 | # rhodecode | |
|
19 | sys.exit(0) | |
|
20 | ||
|
21 | repo_path = os.path.abspath('.') | |
|
22 | push_data = sys.stdin.readlines() | |
|
23 | # os.environ is modified here by a subprocess call that | |
|
24 | # runs git and later git executes this hook. | |
|
25 | # Environ get's some additional info from rhodecode system | |
|
26 | # like IP or username from basic-auth | |
|
27 | handle_git_pre_receive(repo_path, push_data, os.environ) | |
|
28 | sys.exit(0) | |
|
29 | ||
|
30 | if __name__ == '__main__': | |
|
31 | main() |
@@ -22,6 +22,7 b' Users Guide' | |||
|
22 | 22 | usage/general |
|
23 | 23 | usage/git_support |
|
24 | 24 | usage/performance |
|
25 | usage/locking | |
|
25 | 26 | usage/statistics |
|
26 | 27 | usage/backup |
|
27 | 28 | usage/debugging |
@@ -138,7 +138,9 b' def make_map(config):' | |||
|
138 | 138 | m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*?}", |
|
139 | 139 | action="repo_as_fork", conditions=dict(method=["PUT"], |
|
140 | 140 | function=check_repo)) |
|
141 | ||
|
141 | m.connect('repo_locking', "/repo_locking/{repo_name:.*?}", | |
|
142 | action="repo_locking", conditions=dict(method=["PUT"], | |
|
143 | function=check_repo)) | |
|
142 | 144 | with rmap.submapper(path_prefix=ADMIN_PREFIX, |
|
143 | 145 | controller='admin/repos_groups') as m: |
|
144 | 146 | m.connect("repos_groups", "/repos_groups", |
@@ -381,6 +381,7 b' class ReposController(BaseController):' | |||
|
381 | 381 | RepoModel().delete_stats(repo_name) |
|
382 | 382 | Session().commit() |
|
383 | 383 | except Exception, e: |
|
384 | log.error(traceback.format_exc()) | |
|
384 | 385 | h.flash(_('An error occurred during deletion of repository stats'), |
|
385 | 386 | category='error') |
|
386 | 387 | return redirect(url('edit_repo', repo_name=repo_name)) |
@@ -397,11 +398,32 b' class ReposController(BaseController):' | |||
|
397 | 398 | ScmModel().mark_for_invalidation(repo_name) |
|
398 | 399 | Session().commit() |
|
399 | 400 | except Exception, e: |
|
401 | log.error(traceback.format_exc()) | |
|
400 | 402 | h.flash(_('An error occurred during cache invalidation'), |
|
401 | 403 | category='error') |
|
402 | 404 | return redirect(url('edit_repo', repo_name=repo_name)) |
|
403 | 405 | |
|
404 | 406 | @HasPermissionAllDecorator('hg.admin') |
|
407 | def repo_locking(self, repo_name): | |
|
408 | """ | |
|
409 | Unlock repository when it is locked ! | |
|
410 | ||
|
411 | :param repo_name: | |
|
412 | """ | |
|
413 | ||
|
414 | try: | |
|
415 | repo = Repository.get_by_repo_name(repo_name) | |
|
416 | if request.POST.get('set_lock'): | |
|
417 | Repository.lock(repo, c.rhodecode_user.user_id) | |
|
418 | elif request.POST.get('set_unlock'): | |
|
419 | Repository.unlock(repo) | |
|
420 | except Exception, e: | |
|
421 | log.error(traceback.format_exc()) | |
|
422 | h.flash(_('An error occurred during unlocking'), | |
|
423 | category='error') | |
|
424 | return redirect(url('edit_repo', repo_name=repo_name)) | |
|
425 | ||
|
426 | @HasPermissionAllDecorator('hg.admin') | |
|
405 | 427 | def repo_public_journal(self, repo_name): |
|
406 | 428 | """ |
|
407 | 429 | Set's this repository to be visible in public journal, |
@@ -807,7 +807,7 b' class HasPermissionAnyMiddleware(object)' | |||
|
807 | 807 | return self.check_permissions() |
|
808 | 808 | |
|
809 | 809 | def check_permissions(self): |
|
810 |
log.debug('checking |
|
|
810 | log.debug('checking VCS protocol ' | |
|
811 | 811 | 'permissions %s for user:%s repository:%s', self.user_perms, |
|
812 | 812 | self.username, self.repo_name) |
|
813 | 813 | if self.required_perms.intersection(self.user_perms): |
@@ -8,6 +8,7 b' import traceback' | |||
|
8 | 8 | |
|
9 | 9 | from paste.auth.basic import AuthBasicAuthenticator |
|
10 | 10 | from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden |
|
11 | from webob.exc import HTTPClientError | |
|
11 | 12 | from paste.httpheaders import WWW_AUTHENTICATE |
|
12 | 13 | |
|
13 | 14 | from pylons import config, tmpl_context as c, request, session, url |
@@ -17,15 +18,17 b' from pylons.templating import render_mak' | |||
|
17 | 18 | |
|
18 | 19 | from rhodecode import __version__, BACKENDS |
|
19 | 20 | |
|
20 | from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict | |
|
21 | from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\ | |
|
22 | safe_str | |
|
21 | 23 | from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\ |
|
22 | 24 | HasPermissionAnyMiddleware, CookieStoreWrapper |
|
23 | 25 | from rhodecode.lib.utils import get_repo_slug, invalidate_cache |
|
24 | 26 | from rhodecode.model import meta |
|
25 | 27 | |
|
26 | from rhodecode.model.db import Repository, RhodeCodeUi | |
|
28 | from rhodecode.model.db import Repository, RhodeCodeUi, User | |
|
27 | 29 | from rhodecode.model.notification import NotificationModel |
|
28 | 30 | from rhodecode.model.scm import ScmModel |
|
31 | from rhodecode.model.meta import Session | |
|
29 | 32 | |
|
30 | 33 | log = logging.getLogger(__name__) |
|
31 | 34 | |
@@ -159,6 +162,49 b' class BaseVCSController(object):' | |||
|
159 | 162 | return False |
|
160 | 163 | return True |
|
161 | 164 | |
|
165 | def _check_locking_state(self, environ, action, repo, user_id): | |
|
166 | """ | |
|
167 | Checks locking on this repository, if locking is enabled and lock is | |
|
168 | present returns a tuple of make_lock, locked, locked_by. | |
|
169 | make_lock can have 3 states None (do nothing) True, make lock | |
|
170 | False release lock, This value is later propagated to hooks, which | |
|
171 | do the locking. Think about this as signals passed to hooks what to do. | |
|
172 | ||
|
173 | """ | |
|
174 | locked = False | |
|
175 | make_lock = None | |
|
176 | repo = Repository.get_by_repo_name(repo) | |
|
177 | user = User.get(user_id) | |
|
178 | ||
|
179 | # this is kind of hacky, but due to how mercurial handles client-server | |
|
180 | # server see all operation on changeset; bookmarks, phases and | |
|
181 | # obsolescence marker in different transaction, we don't want to check | |
|
182 | # locking on those | |
|
183 | obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',] | |
|
184 | locked_by = repo.locked | |
|
185 | if repo and repo.enable_locking and not obsolete_call: | |
|
186 | if action == 'push': | |
|
187 | #check if it's already locked !, if it is compare users | |
|
188 | user_id, _date = repo.locked | |
|
189 | if user.user_id == user_id: | |
|
190 | log.debug('Got push from user, now unlocking' % (user)) | |
|
191 | # unlock if we have push from user who locked | |
|
192 | make_lock = False | |
|
193 | else: | |
|
194 | # we're not the same user who locked, ban with 423 ! | |
|
195 | locked = True | |
|
196 | if action == 'pull': | |
|
197 | if repo.locked[0] and repo.locked[1]: | |
|
198 | locked = True | |
|
199 | else: | |
|
200 | log.debug('Setting lock on repo %s by %s' % (repo, user)) | |
|
201 | make_lock = True | |
|
202 | ||
|
203 | else: | |
|
204 | log.debug('Repository %s do not have locking enabled' % (repo)) | |
|
205 | ||
|
206 | return make_lock, locked, locked_by | |
|
207 | ||
|
162 | 208 | def __call__(self, environ, start_response): |
|
163 | 209 | start = time.time() |
|
164 | 210 | try: |
@@ -307,37 +307,47 b' class DbManage(object):' | |||
|
307 | 307 | hooks1.ui_key = hooks1_key |
|
308 | 308 | hooks1.ui_value = 'hg update >&2' |
|
309 | 309 | hooks1.ui_active = False |
|
310 | self.sa.add(hooks1) | |
|
310 | 311 | |
|
311 | 312 | hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE |
|
312 | 313 | hooks2_ = self.sa.query(RhodeCodeUi)\ |
|
313 | 314 | .filter(RhodeCodeUi.ui_key == hooks2_key).scalar() |
|
314 | ||
|
315 | 315 | hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_ |
|
316 | 316 | hooks2.ui_section = 'hooks' |
|
317 | 317 | hooks2.ui_key = hooks2_key |
|
318 | 318 | hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size' |
|
319 | self.sa.add(hooks2) | |
|
319 | 320 | |
|
320 | 321 | hooks3 = RhodeCodeUi() |
|
321 | 322 | hooks3.ui_section = 'hooks' |
|
322 | 323 | hooks3.ui_key = RhodeCodeUi.HOOK_PUSH |
|
323 | 324 | hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action' |
|
325 | self.sa.add(hooks3) | |
|
324 | 326 | |
|
325 | 327 | hooks4 = RhodeCodeUi() |
|
326 | 328 | hooks4.ui_section = 'hooks' |
|
327 |
hooks4.ui_key = RhodeCodeUi.HOOK_P |
|
|
328 |
hooks4.ui_value = 'python:rhodecode.lib.hooks. |
|
|
329 | hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH | |
|
330 | hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push' | |
|
331 | self.sa.add(hooks4) | |
|
329 | 332 | |
|
330 | # For mercurial 1.7 set backward comapatibility with format | |
|
331 | dotencode_disable = RhodeCodeUi() | |
|
332 | dotencode_disable.ui_section = 'format' | |
|
333 | dotencode_disable.ui_key = 'dotencode' | |
|
334 | dotencode_disable.ui_value = 'false' | |
|
333 | hooks5 = RhodeCodeUi() | |
|
334 | hooks5.ui_section = 'hooks' | |
|
335 | hooks5.ui_key = RhodeCodeUi.HOOK_PULL | |
|
336 | hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action' | |
|
337 | self.sa.add(hooks5) | |
|
338 | ||
|
339 | hooks6 = RhodeCodeUi() | |
|
340 | hooks6.ui_section = 'hooks' | |
|
341 | hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL | |
|
342 | hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull' | |
|
343 | self.sa.add(hooks6) | |
|
335 | 344 | |
|
336 | 345 | # enable largefiles |
|
337 | 346 | largefiles = RhodeCodeUi() |
|
338 | 347 | largefiles.ui_section = 'extensions' |
|
339 | 348 | largefiles.ui_key = 'largefiles' |
|
340 | 349 | largefiles.ui_value = '' |
|
350 | self.sa.add(largefiles) | |
|
341 | 351 | |
|
342 | 352 | # enable hgsubversion disabled by default |
|
343 | 353 | hgsubversion = RhodeCodeUi() |
@@ -345,6 +355,7 b' class DbManage(object):' | |||
|
345 | 355 | hgsubversion.ui_key = 'hgsubversion' |
|
346 | 356 | hgsubversion.ui_value = '' |
|
347 | 357 | hgsubversion.ui_active = False |
|
358 | self.sa.add(hgsubversion) | |
|
348 | 359 | |
|
349 | 360 | # enable hggit disabled by default |
|
350 | 361 | hggit = RhodeCodeUi() |
@@ -352,13 +363,6 b' class DbManage(object):' | |||
|
352 | 363 | hggit.ui_key = 'hggit' |
|
353 | 364 | hggit.ui_value = '' |
|
354 | 365 | hggit.ui_active = False |
|
355 | ||
|
356 | self.sa.add(hooks1) | |
|
357 | self.sa.add(hooks2) | |
|
358 | self.sa.add(hooks3) | |
|
359 | self.sa.add(hooks4) | |
|
360 | self.sa.add(largefiles) | |
|
361 | self.sa.add(hgsubversion) | |
|
362 | 366 | self.sa.add(hggit) |
|
363 | 367 | |
|
364 | 368 | def create_ldap_options(self, skip_existing=False): |
@@ -461,6 +465,11 b' class DbManage(object):' | |||
|
461 | 465 | paths.ui_key = '/' |
|
462 | 466 | paths.ui_value = path |
|
463 | 467 | |
|
468 | phases = RhodeCodeUi() | |
|
469 | phases.ui_section = 'phases' | |
|
470 | phases.ui_key = 'publish' | |
|
471 | phases.ui_value = False | |
|
472 | ||
|
464 | 473 | sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication') |
|
465 | 474 | sett2 = RhodeCodeSetting('title', 'RhodeCode') |
|
466 | 475 | sett3 = RhodeCodeSetting('ga_code', '') |
@@ -23,6 +23,8 b'' | |||
|
23 | 23 | # You should have received a copy of the GNU General Public License |
|
24 | 24 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
25 | 25 | |
|
26 | from webob.exc import HTTPClientError | |
|
27 | ||
|
26 | 28 | |
|
27 | 29 | class LdapUsernameError(Exception): |
|
28 | 30 | pass |
@@ -53,4 +55,17 b' class UsersGroupsAssignedException(Excep' | |||
|
53 | 55 | |
|
54 | 56 | |
|
55 | 57 | class StatusChangeOnClosedPullRequestError(Exception): |
|
56 | pass No newline at end of file | |
|
58 | pass | |
|
59 | ||
|
60 | ||
|
61 | class HTTPLockedRC(HTTPClientError): | |
|
62 | """ | |
|
63 | Special Exception For locked Repos in RhodeCode | |
|
64 | """ | |
|
65 | code = 423 | |
|
66 | title = explanation = 'Repository Locked' | |
|
67 | ||
|
68 | def __init__(self, reponame, username, *args, **kwargs): | |
|
69 | self.title = self.explanation = ('Repository `%s` locked by ' | |
|
70 | 'user `%s`' % (reponame, username)) | |
|
71 | super(HTTPLockedRC, self).__init__(*args, **kwargs) |
@@ -41,7 +41,7 b' from webhelpers.html.tags import _set_in' | |||
|
41 | 41 | from rhodecode.lib.annotate import annotate_highlight |
|
42 | 42 | from rhodecode.lib.utils import repo_name_slug |
|
43 | 43 | from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ |
|
44 | get_changeset_safe | |
|
44 | get_changeset_safe, datetime_to_time, time_to_datetime | |
|
45 | 45 | from rhodecode.lib.markup_renderer import MarkupRenderer |
|
46 | 46 | from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError |
|
47 | 47 | from rhodecode.lib.vcs.backends.base import BaseChangeset |
@@ -439,6 +439,19 b' def person(author):' | |||
|
439 | 439 | return _author |
|
440 | 440 | |
|
441 | 441 | |
|
442 | def person_by_id(id_): | |
|
443 | # attr to return from fetched user | |
|
444 | person_getter = lambda usr: usr.username | |
|
445 | ||
|
446 | #maybe it's an ID ? | |
|
447 | if str(id_).isdigit() or isinstance(id_, int): | |
|
448 | id_ = int(id_) | |
|
449 | user = User.get(id_) | |
|
450 | if user is not None: | |
|
451 | return person_getter(user) | |
|
452 | return id_ | |
|
453 | ||
|
454 | ||
|
442 | 455 | def desc_stylize(value): |
|
443 | 456 | """ |
|
444 | 457 | converts tags from value into html equivalent |
@@ -34,6 +34,9 b' from rhodecode.lib import helpers as h' | |||
|
34 | 34 | from rhodecode.lib.utils import action_logger |
|
35 | 35 | from rhodecode.lib.vcs.backends.base import EmptyChangeset |
|
36 | 36 | from rhodecode.lib.compat import json |
|
37 | from rhodecode.model.db import Repository, User | |
|
38 | from rhodecode.lib.utils2 import safe_str | |
|
39 | from rhodecode.lib.exceptions import HTTPLockedRC | |
|
37 | 40 | |
|
38 | 41 | |
|
39 | 42 | def _get_scm_size(alias, root_path): |
@@ -84,6 +87,59 b' def repo_size(ui, repo, hooktype=None, *' | |||
|
84 | 87 | sys.stdout.write(msg) |
|
85 | 88 | |
|
86 | 89 | |
|
90 | def pre_push(ui, repo, **kwargs): | |
|
91 | # pre push function, currently used to ban pushing when | |
|
92 | # repository is locked | |
|
93 | try: | |
|
94 | rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}")) | |
|
95 | except: | |
|
96 | rc_extras = {} | |
|
97 | extras = dict(repo.ui.configitems('rhodecode_extras')) | |
|
98 | ||
|
99 | if 'username' in extras: | |
|
100 | username = extras['username'] | |
|
101 | repository = extras['repository'] | |
|
102 | scm = extras['scm'] | |
|
103 | locked_by = extras['locked_by'] | |
|
104 | elif 'username' in rc_extras: | |
|
105 | username = rc_extras['username'] | |
|
106 | repository = rc_extras['repository'] | |
|
107 | scm = rc_extras['scm'] | |
|
108 | locked_by = rc_extras['locked_by'] | |
|
109 | else: | |
|
110 | raise Exception('Missing data in repo.ui and os.environ') | |
|
111 | ||
|
112 | usr = User.get_by_username(username) | |
|
113 | ||
|
114 | if locked_by[0] and usr.user_id != int(locked_by[0]): | |
|
115 | raise HTTPLockedRC(username, repository) | |
|
116 | ||
|
117 | ||
|
118 | def pre_pull(ui, repo, **kwargs): | |
|
119 | # pre push function, currently used to ban pushing when | |
|
120 | # repository is locked | |
|
121 | try: | |
|
122 | rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}")) | |
|
123 | except: | |
|
124 | rc_extras = {} | |
|
125 | extras = dict(repo.ui.configitems('rhodecode_extras')) | |
|
126 | if 'username' in extras: | |
|
127 | username = extras['username'] | |
|
128 | repository = extras['repository'] | |
|
129 | scm = extras['scm'] | |
|
130 | locked_by = extras['locked_by'] | |
|
131 | elif 'username' in rc_extras: | |
|
132 | username = rc_extras['username'] | |
|
133 | repository = rc_extras['repository'] | |
|
134 | scm = rc_extras['scm'] | |
|
135 | locked_by = rc_extras['locked_by'] | |
|
136 | else: | |
|
137 | raise Exception('Missing data in repo.ui and os.environ') | |
|
138 | ||
|
139 | if locked_by[0]: | |
|
140 | raise HTTPLockedRC(username, repository) | |
|
141 | ||
|
142 | ||
|
87 | 143 | def log_pull_action(ui, repo, **kwargs): |
|
88 | 144 | """ |
|
89 | 145 | Logs user last pull action |
@@ -100,15 +156,17 b' def log_pull_action(ui, repo, **kwargs):' | |||
|
100 | 156 | username = extras['username'] |
|
101 | 157 | repository = extras['repository'] |
|
102 | 158 | scm = extras['scm'] |
|
159 | make_lock = extras['make_lock'] | |
|
103 | 160 | elif 'username' in rc_extras: |
|
104 | 161 | username = rc_extras['username'] |
|
105 | 162 | repository = rc_extras['repository'] |
|
106 | 163 | scm = rc_extras['scm'] |
|
164 | make_lock = rc_extras['make_lock'] | |
|
107 | 165 | else: |
|
108 | 166 | raise Exception('Missing data in repo.ui and os.environ') |
|
109 | ||
|
167 | user = User.get_by_username(username) | |
|
110 | 168 | action = 'pull' |
|
111 |
action_logger(user |
|
|
169 | action_logger(user, action, repository, extras['ip'], commit=True) | |
|
112 | 170 | # extension hook call |
|
113 | 171 | from rhodecode import EXTENSIONS |
|
114 | 172 | callback = getattr(EXTENSIONS, 'PULL_HOOK', None) |
@@ -117,6 +175,12 b' def log_pull_action(ui, repo, **kwargs):' | |||
|
117 | 175 | kw = {} |
|
118 | 176 | kw.update(extras) |
|
119 | 177 | callback(**kw) |
|
178 | ||
|
179 | if make_lock is True: | |
|
180 | Repository.lock(Repository.get_by_repo_name(repository), user.user_id) | |
|
181 | #msg = 'Made lock on repo `%s`' % repository | |
|
182 | #sys.stdout.write(msg) | |
|
183 | ||
|
120 | 184 | return 0 |
|
121 | 185 | |
|
122 | 186 | |
@@ -138,10 +202,12 b' def log_push_action(ui, repo, **kwargs):' | |||
|
138 | 202 | username = extras['username'] |
|
139 | 203 | repository = extras['repository'] |
|
140 | 204 | scm = extras['scm'] |
|
205 | make_lock = extras['make_lock'] | |
|
141 | 206 | elif 'username' in rc_extras: |
|
142 | 207 | username = rc_extras['username'] |
|
143 | 208 | repository = rc_extras['repository'] |
|
144 | 209 | scm = rc_extras['scm'] |
|
210 | make_lock = rc_extras['make_lock'] | |
|
145 | 211 | else: |
|
146 | 212 | raise Exception('Missing data in repo.ui and os.environ') |
|
147 | 213 | |
@@ -179,6 +245,12 b' def log_push_action(ui, repo, **kwargs):' | |||
|
179 | 245 | kw = {'pushed_revs': revs} |
|
180 | 246 | kw.update(extras) |
|
181 | 247 | callback(**kw) |
|
248 | ||
|
249 | if make_lock is False: | |
|
250 | Repository.unlock(Repository.get_by_repo_name(repository)) | |
|
251 | msg = 'Released lock on repo `%s`\n' % repository | |
|
252 | sys.stdout.write(msg) | |
|
253 | ||
|
182 | 254 | return 0 |
|
183 | 255 | |
|
184 | 256 | |
@@ -219,8 +291,13 b' def log_create_repository(repository_dic' | |||
|
219 | 291 | |
|
220 | 292 | return 0 |
|
221 | 293 | |
|
294 | handle_git_pre_receive = (lambda repo_path, revs, env: | |
|
295 | handle_git_receive(repo_path, revs, env, hook_type='pre')) | |
|
296 | handle_git_post_receive = (lambda repo_path, revs, env: | |
|
297 | handle_git_receive(repo_path, revs, env, hook_type='post')) | |
|
222 | 298 | |
|
223 | def handle_git_post_receive(repo_path, revs, env): | |
|
299 | ||
|
300 | def handle_git_receive(repo_path, revs, env, hook_type='post'): | |
|
224 | 301 | """ |
|
225 | 302 | A really hacky method that is runned by git post-receive hook and logs |
|
226 | 303 | an push action together with pushed revisions. It's executed by subprocess |
@@ -240,7 +317,6 b' def handle_git_post_receive(repo_path, r' | |||
|
240 | 317 | from rhodecode.model import init_model |
|
241 | 318 | from rhodecode.model.db import RhodeCodeUi |
|
242 | 319 | from rhodecode.lib.utils import make_ui |
|
243 | from rhodecode.model.db import Repository | |
|
244 | 320 | |
|
245 | 321 | path, ini_name = os.path.split(env['RHODECODE_CONFIG_FILE']) |
|
246 | 322 | conf = appconfig('config:%s' % ini_name, relative_to=path) |
@@ -255,21 +331,19 b' def handle_git_post_receive(repo_path, r' | |||
|
255 | 331 | repo_path = repo_path[:-4] |
|
256 | 332 | repo = Repository.get_by_full_path(repo_path) |
|
257 | 333 | _hooks = dict(baseui.configitems('hooks')) or {} |
|
258 | # if push hook is enabled via web interface | |
|
259 | if repo and _hooks.get(RhodeCodeUi.HOOK_PUSH): | |
|
260 | 334 | |
|
261 | extras = { | |
|
262 | 'username': env['RHODECODE_USER'], | |
|
263 | 'repository': repo.repo_name, | |
|
264 | 'scm': 'git', | |
|
265 | 'action': 'push', | |
|
266 | 'ip': env['RHODECODE_CONFIG_IP'], | |
|
267 | } | |
|
335 | extras = json.loads(env['RHODECODE_EXTRAS']) | |
|
268 | 336 |
|
|
269 | 337 |
|
|
270 | 338 |
|
|
271 | 339 |
|
|
272 | 340 | |
|
341 | if hook_type == 'pre': | |
|
342 | pre_push(baseui, repo) | |
|
343 | ||
|
344 | # if push hook is enabled via web interface | |
|
345 | elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH): | |
|
346 | ||
|
273 | 347 | rev_data = [] |
|
274 | 348 | for l in revs: |
|
275 | 349 | old_rev, new_rev, ref = l.split(' ') |
@@ -41,7 +41,7 b' class GitRepository(object):' | |||
|
41 | 41 | git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs']) |
|
42 | 42 | commands = ['git-upload-pack', 'git-receive-pack'] |
|
43 | 43 | |
|
44 |
def __init__(self, repo_name, content_path, |
|
|
44 | def __init__(self, repo_name, content_path, extras): | |
|
45 | 45 | files = set([f.lower() for f in os.listdir(content_path)]) |
|
46 | 46 | if not (self.git_folder_signature.intersection(files) |
|
47 | 47 | == self.git_folder_signature): |
@@ -50,7 +50,7 b' class GitRepository(object):' | |||
|
50 | 50 | self.valid_accepts = ['application/x-%s-result' % |
|
51 | 51 | c for c in self.commands] |
|
52 | 52 | self.repo_name = repo_name |
|
53 |
self. |
|
|
53 | self.extras = extras | |
|
54 | 54 | |
|
55 | 55 | def _get_fixedpath(self, path): |
|
56 | 56 | """ |
@@ -67,7 +67,7 b' class GitRepository(object):' | |||
|
67 | 67 | HTTP /info/refs request. |
|
68 | 68 | """ |
|
69 | 69 | |
|
70 |
git_command = request.GET |
|
|
70 | git_command = request.GET.get('service') | |
|
71 | 71 | if git_command not in self.commands: |
|
72 | 72 | log.debug('command %s not allowed' % git_command) |
|
73 | 73 | return exc.HTTPMethodNotAllowed() |
@@ -119,9 +119,8 b' class GitRepository(object):' | |||
|
119 | 119 | try: |
|
120 | 120 | gitenv = os.environ |
|
121 | 121 | from rhodecode import CONFIG |
|
122 |
from rhodecode.lib. |
|
|
123 |
gitenv['RHODECODE_ |
|
|
124 | gitenv['RHODECODE_CONFIG_IP'] = _get_ip_addr(environ) | |
|
122 | from rhodecode.lib.compat import json | |
|
123 | gitenv['RHODECODE_EXTRAS'] = json.dumps(self.extras) | |
|
125 | 124 | # forget all configs |
|
126 | 125 | gitenv['GIT_CONFIG_NOGLOBAL'] = '1' |
|
127 | 126 | # we need current .ini file used to later initialize rhodecode |
@@ -174,7 +173,7 b' class GitRepository(object):' | |||
|
174 | 173 | |
|
175 | 174 | class GitDirectory(object): |
|
176 | 175 | |
|
177 |
def __init__(self, repo_root, repo_name, |
|
|
176 | def __init__(self, repo_root, repo_name, extras): | |
|
178 | 177 | repo_location = os.path.join(repo_root, repo_name) |
|
179 | 178 | if not os.path.isdir(repo_location): |
|
180 | 179 | raise OSError(repo_location) |
@@ -182,12 +181,12 b' class GitDirectory(object):' | |||
|
182 | 181 | self.content_path = repo_location |
|
183 | 182 | self.repo_name = repo_name |
|
184 | 183 | self.repo_location = repo_location |
|
185 |
self. |
|
|
184 | self.extras = extras | |
|
186 | 185 | |
|
187 | 186 | def __call__(self, environ, start_response): |
|
188 | 187 | content_path = self.content_path |
|
189 | 188 | try: |
|
190 |
app = GitRepository(self.repo_name, content_path, self. |
|
|
189 | app = GitRepository(self.repo_name, content_path, self.extras) | |
|
191 | 190 | except (AssertionError, OSError): |
|
192 | 191 | if os.path.isdir(os.path.join(content_path, '.git')): |
|
193 | 192 | app = GitRepository(self.repo_name, |
@@ -198,5 +197,5 b' class GitDirectory(object):' | |||
|
198 | 197 | return app(environ, start_response) |
|
199 | 198 | |
|
200 | 199 | |
|
201 |
def make_wsgi_app(repo_name, repo_root, |
|
|
202 |
return GitDirectory(repo_root, repo_name, |
|
|
200 | def make_wsgi_app(repo_name, repo_root, extras): | |
|
201 | return GitDirectory(repo_root, repo_name, extras) |
@@ -31,6 +31,8 b' import traceback' | |||
|
31 | 31 | |
|
32 | 32 | from dulwich import server as dulserver |
|
33 | 33 | from dulwich.web import LimitedInputFilter, GunzipFilter |
|
34 | from rhodecode.lib.exceptions import HTTPLockedRC | |
|
35 | from rhodecode.lib.hooks import pre_pull | |
|
34 | 36 | |
|
35 | 37 | |
|
36 | 38 | class SimpleGitUploadPackHandler(dulserver.UploadPackHandler): |
@@ -102,11 +104,11 b' def is_git(environ):' | |||
|
102 | 104 | class SimpleGit(BaseVCSController): |
|
103 | 105 | |
|
104 | 106 | def _handle_request(self, environ, start_response): |
|
105 | ||
|
106 | 107 | if not is_git(environ): |
|
107 | 108 | return self.application(environ, start_response) |
|
108 | 109 | if not self._check_ssl(environ, start_response): |
|
109 | 110 | return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response) |
|
111 | ||
|
110 | 112 | ipaddr = self._get_ip_addr(environ) |
|
111 | 113 | username = None |
|
112 | 114 | self._git_first_op = False |
@@ -184,21 +186,39 b' class SimpleGit(BaseVCSController):' | |||
|
184 | 186 | if perm is not True: |
|
185 | 187 | return HTTPForbidden()(environ, start_response) |
|
186 | 188 | |
|
189 | # extras are injected into UI object and later available | |
|
190 | # in hooks executed by rhodecode | |
|
187 | 191 | extras = { |
|
188 | 192 | 'ip': ipaddr, |
|
189 | 193 | 'username': username, |
|
190 | 194 | 'action': action, |
|
191 | 195 | 'repository': repo_name, |
|
192 | 196 | 'scm': 'git', |
|
197 | 'make_lock': None, | |
|
198 | 'locked_by': [None, None] | |
|
193 | 199 | } |
|
194 | # set the environ variables for this request | |
|
195 | os.environ['RC_SCM_DATA'] = json.dumps(extras) | |
|
200 | ||
|
196 | 201 | #=================================================================== |
|
197 | 202 | # GIT REQUEST HANDLING |
|
198 | 203 | #=================================================================== |
|
199 | 204 | repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) |
|
200 | 205 | log.debug('Repository path is %s' % repo_path) |
|
201 | 206 | |
|
207 | # CHECK LOCKING only if it's not ANONYMOUS USER | |
|
208 | if username != User.DEFAULT_USER: | |
|
209 | log.debug('Checking locking on repository') | |
|
210 | (make_lock, | |
|
211 | locked, | |
|
212 | locked_by) = self._check_locking_state( | |
|
213 | environ=environ, action=action, | |
|
214 | repo=repo_name, user_id=user.user_id | |
|
215 | ) | |
|
216 | # store the make_lock for later evaluation in hooks | |
|
217 | extras.update({'make_lock': make_lock, | |
|
218 | 'locked_by': locked_by}) | |
|
219 | # set the environ variables for this request | |
|
220 | os.environ['RC_SCM_DATA'] = json.dumps(extras) | |
|
221 | log.debug('HOOKS extras is %s' % extras) | |
|
202 | 222 | baseui = make_ui('db') |
|
203 | 223 | self.__inject_extras(repo_path, baseui, extras) |
|
204 | 224 | |
@@ -209,13 +229,16 b' class SimpleGit(BaseVCSController):' | |||
|
209 | 229 | self._handle_githooks(repo_name, action, baseui, environ) |
|
210 | 230 | |
|
211 | 231 | log.info('%s action on GIT repo "%s"' % (action, repo_name)) |
|
212 |
app = self.__make_app(repo_name, repo_path, |
|
|
232 | app = self.__make_app(repo_name, repo_path, extras) | |
|
213 | 233 | return app(environ, start_response) |
|
234 | except HTTPLockedRC, e: | |
|
235 | log.debug('Repositry LOCKED ret code 423!') | |
|
236 | return e(environ, start_response) | |
|
214 | 237 | except Exception: |
|
215 | 238 | log.error(traceback.format_exc()) |
|
216 | 239 | return HTTPInternalServerError()(environ, start_response) |
|
217 | 240 | |
|
218 |
def __make_app(self, repo_name, repo_path, |
|
|
241 | def __make_app(self, repo_name, repo_path, extras): | |
|
219 | 242 | """ |
|
220 | 243 | Make an wsgi application using dulserver |
|
221 | 244 | |
@@ -227,7 +250,7 b' class SimpleGit(BaseVCSController):' | |||
|
227 | 250 | app = make_wsgi_app( |
|
228 | 251 | repo_root=safe_str(self.basepath), |
|
229 | 252 | repo_name=repo_name, |
|
230 |
|
|
|
253 | extras=extras, | |
|
231 | 254 | ) |
|
232 | 255 | app = GunzipFilter(LimitedInputFilter(app)) |
|
233 | 256 | return app |
@@ -279,6 +302,7 b' class SimpleGit(BaseVCSController):' | |||
|
279 | 302 | """ |
|
280 | 303 | from rhodecode.lib.hooks import log_pull_action |
|
281 | 304 | service = environ['QUERY_STRING'].split('=') |
|
305 | ||
|
282 | 306 | if len(service) < 2: |
|
283 | 307 | return |
|
284 | 308 | |
@@ -288,6 +312,9 b' class SimpleGit(BaseVCSController):' | |||
|
288 | 312 | _repo._repo.ui = baseui |
|
289 | 313 | |
|
290 | 314 | _hooks = dict(baseui.configitems('hooks')) or {} |
|
315 | if action == 'pull': | |
|
316 | # stupid git, emulate pre-pull hook ! | |
|
317 | pre_pull(ui=baseui, repo=_repo._repo) | |
|
291 | 318 | if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL): |
|
292 | 319 | log_pull_action(ui=baseui, repo=_repo._repo) |
|
293 | 320 |
@@ -42,6 +42,7 b' from rhodecode.lib.auth import get_conta' | |||
|
42 | 42 | from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections |
|
43 | 43 | from rhodecode.lib.compat import json |
|
44 | 44 | from rhodecode.model.db import User |
|
45 | from rhodecode.lib.exceptions import HTTPLockedRC | |
|
45 | 46 | |
|
46 | 47 | |
|
47 | 48 | log = logging.getLogger(__name__) |
@@ -157,15 +158,31 b' class SimpleHg(BaseVCSController):' | |||
|
157 | 158 | 'action': action, |
|
158 | 159 | 'repository': repo_name, |
|
159 | 160 | 'scm': 'hg', |
|
161 | 'make_lock': None, | |
|
162 | 'locked_by': [None, None] | |
|
160 | 163 | } |
|
161 | # set the environ variables for this request | |
|
162 | os.environ['RC_SCM_DATA'] = json.dumps(extras) | |
|
163 | 164 | #====================================================================== |
|
164 | 165 | # MERCURIAL REQUEST HANDLING |
|
165 | 166 | #====================================================================== |
|
166 | 167 | repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) |
|
167 | 168 | log.debug('Repository path is %s' % repo_path) |
|
168 | 169 | |
|
170 | # CHECK LOCKING only if it's not ANONYMOUS USER | |
|
171 | if username != User.DEFAULT_USER: | |
|
172 | log.debug('Checking locking on repository') | |
|
173 | (make_lock, | |
|
174 | locked, | |
|
175 | locked_by) = self._check_locking_state( | |
|
176 | environ=environ, action=action, | |
|
177 | repo=repo_name, user_id=user.user_id | |
|
178 | ) | |
|
179 | # store the make_lock for later evaluation in hooks | |
|
180 | extras.update({'make_lock': make_lock, | |
|
181 | 'locked_by': locked_by}) | |
|
182 | ||
|
183 | # set the environ variables for this request | |
|
184 | os.environ['RC_SCM_DATA'] = json.dumps(extras) | |
|
185 | log.debug('HOOKS extras is %s' % extras) | |
|
169 | 186 | baseui = make_ui('db') |
|
170 | 187 | self.__inject_extras(repo_path, baseui, extras) |
|
171 | 188 | |
@@ -179,6 +196,9 b' class SimpleHg(BaseVCSController):' | |||
|
179 | 196 | except RepoError, e: |
|
180 | 197 | if str(e).find('not found') != -1: |
|
181 | 198 | return HTTPNotFound()(environ, start_response) |
|
199 | except HTTPLockedRC, e: | |
|
200 | log.debug('Repositry LOCKED ret code 423!') | |
|
201 | return e(environ, start_response) | |
|
182 | 202 | except Exception: |
|
183 | 203 | log.error(traceback.format_exc()) |
|
184 | 204 | return HTTPInternalServerError()(environ, start_response) |
@@ -25,7 +25,7 b'' | |||
|
25 | 25 | |
|
26 | 26 | import re |
|
27 | 27 | import time |
|
28 |
|
|
|
28 | import datetime | |
|
29 | 29 | from pylons.i18n.translation import _, ungettext |
|
30 | 30 | from rhodecode.lib.vcs.utils.lazy import LazyProperty |
|
31 | 31 | |
@@ -300,7 +300,7 b' def age(prevdate):' | |||
|
300 | 300 | deltas = {} |
|
301 | 301 | |
|
302 | 302 | # Get date parts deltas |
|
303 | now = datetime.now() | |
|
303 | now = datetime.datetime.now() | |
|
304 | 304 | for part in order: |
|
305 | 305 | deltas[part] = getattr(now, part) - getattr(prevdate, part) |
|
306 | 306 | |
@@ -435,6 +435,15 b' def datetime_to_time(dt):' | |||
|
435 | 435 | return time.mktime(dt.timetuple()) |
|
436 | 436 | |
|
437 | 437 | |
|
438 | def time_to_datetime(tm): | |
|
439 | if tm: | |
|
440 | if isinstance(tm, basestring): | |
|
441 | try: | |
|
442 | tm = float(tm) | |
|
443 | except ValueError: | |
|
444 | return | |
|
445 | return datetime.datetime.fromtimestamp(tm) | |
|
446 | ||
|
438 | 447 | MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})' |
|
439 | 448 | |
|
440 | 449 |
@@ -28,6 +28,7 b' import logging' | |||
|
28 | 28 | import datetime |
|
29 | 29 | import traceback |
|
30 | 30 | import hashlib |
|
31 | import time | |
|
31 | 32 | from collections import defaultdict |
|
32 | 33 | |
|
33 | 34 | from sqlalchemy import * |
@@ -232,7 +233,9 b' class RhodeCodeUi(Base, BaseModel):' | |||
|
232 | 233 | HOOK_UPDATE = 'changegroup.update' |
|
233 | 234 | HOOK_REPO_SIZE = 'changegroup.repo_size' |
|
234 | 235 | HOOK_PUSH = 'changegroup.push_logger' |
|
235 | HOOK_PULL = 'preoutgoing.pull_logger' | |
|
236 | HOOK_PRE_PUSH = 'prechangegroup.pre_push' | |
|
237 | HOOK_PULL = 'outgoing.pull_logger' | |
|
238 | HOOK_PRE_PULL = 'preoutgoing.pre_pull' | |
|
236 | 239 | |
|
237 | 240 | ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
238 | 241 | ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) |
@@ -247,17 +250,17 b' class RhodeCodeUi(Base, BaseModel):' | |||
|
247 | 250 | @classmethod |
|
248 | 251 | def get_builtin_hooks(cls): |
|
249 | 252 | q = cls.query() |
|
250 | q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, | |
|
251 |
cls.HOOK_ |
|
|
252 |
cls.HOOK_PU |
|
|
253 | q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE, | |
|
254 | cls.HOOK_PUSH, cls.HOOK_PRE_PUSH, | |
|
255 | cls.HOOK_PULL, cls.HOOK_PRE_PULL])) | |
|
253 | 256 | return q.all() |
|
254 | 257 | |
|
255 | 258 | @classmethod |
|
256 | 259 | def get_custom_hooks(cls): |
|
257 | 260 | q = cls.query() |
|
258 | q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, | |
|
259 |
cls.HOOK_ |
|
|
260 |
cls.HOOK_PU |
|
|
261 | q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE, | |
|
262 | cls.HOOK_PUSH, cls.HOOK_PRE_PUSH, | |
|
263 | cls.HOOK_PULL, cls.HOOK_PRE_PULL])) | |
|
261 | 264 | q = q.filter(cls.ui_section == 'hooks') |
|
262 | 265 | return q.all() |
|
263 | 266 | |
@@ -280,9 +283,13 b' class User(Base, BaseModel):' | |||
|
280 | 283 | __tablename__ = 'users' |
|
281 | 284 | __table_args__ = ( |
|
282 | 285 | UniqueConstraint('username'), UniqueConstraint('email'), |
|
286 | Index('u_username_idx', 'username'), | |
|
287 | Index('u_email_idx', 'email'), | |
|
283 | 288 | {'extend_existing': True, 'mysql_engine': 'InnoDB', |
|
284 | 289 | 'mysql_charset': 'utf8'} |
|
285 | 290 | ) |
|
291 | DEFAULT_USER = 'default' | |
|
292 | ||
|
286 | 293 | user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
287 | 294 | username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) |
|
288 | 295 | password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) |
@@ -572,6 +579,7 b' class Repository(Base, BaseModel):' | |||
|
572 | 579 | __tablename__ = 'repositories' |
|
573 | 580 | __table_args__ = ( |
|
574 | 581 | UniqueConstraint('repo_name'), |
|
582 | Index('r_repo_name_idx', 'repo_name'), | |
|
575 | 583 | {'extend_existing': True, 'mysql_engine': 'InnoDB', |
|
576 | 584 | 'mysql_charset': 'utf8'}, |
|
577 | 585 | ) |
@@ -587,6 +595,8 b' class Repository(Base, BaseModel):' | |||
|
587 | 595 | description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) |
|
588 | 596 | created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) |
|
589 | 597 | landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None) |
|
598 | enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) | |
|
599 | _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None) | |
|
590 | 600 | |
|
591 | 601 | fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None) |
|
592 | 602 | group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None) |
@@ -617,6 +627,21 b' class Repository(Base, BaseModel):' | |||
|
617 | 627 | return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, |
|
618 | 628 | self.repo_name) |
|
619 | 629 | |
|
630 | @hybrid_property | |
|
631 | def locked(self): | |
|
632 | # always should return [user_id, timelocked] | |
|
633 | if self._locked: | |
|
634 | _lock_info = self._locked.split(':') | |
|
635 | return int(_lock_info[0]), _lock_info[1] | |
|
636 | return [None, None] | |
|
637 | ||
|
638 | @locked.setter | |
|
639 | def locked(self, val): | |
|
640 | if val and isinstance(val, (list, tuple)): | |
|
641 | self._locked = ':'.join(map(str, val)) | |
|
642 | else: | |
|
643 | self._locked = None | |
|
644 | ||
|
620 | 645 | @classmethod |
|
621 | 646 | def url_sep(cls): |
|
622 | 647 | return URL_SEP |
@@ -793,6 +818,18 b' class Repository(Base, BaseModel):' | |||
|
793 | 818 | |
|
794 | 819 | return data |
|
795 | 820 | |
|
821 | @classmethod | |
|
822 | def lock(cls, repo, user_id): | |
|
823 | repo.locked = [user_id, time.time()] | |
|
824 | Session().add(repo) | |
|
825 | Session().commit() | |
|
826 | ||
|
827 | @classmethod | |
|
828 | def unlock(cls, repo): | |
|
829 | repo.locked = None | |
|
830 | Session().add(repo) | |
|
831 | Session().commit() | |
|
832 | ||
|
796 | 833 | #========================================================================== |
|
797 | 834 | # SCM PROPERTIES |
|
798 | 835 | #========================================================================== |
@@ -182,6 +182,7 b' def RepoForm(edit=False, old_data={}, su' | |||
|
182 | 182 | private = v.StringBoolean(if_missing=False) |
|
183 | 183 | enable_statistics = v.StringBoolean(if_missing=False) |
|
184 | 184 | enable_downloads = v.StringBoolean(if_missing=False) |
|
185 | enable_locking = v.StringBoolean(if_missing=False) | |
|
185 | 186 | landing_rev = v.OneOf(landing_revs, hideList=True) |
|
186 | 187 | |
|
187 | 188 | if edit: |
@@ -265,7 +266,7 b' def ApplicationUiSettingsForm():' | |||
|
265 | 266 | hooks_changegroup_update = v.StringBoolean(if_missing=False) |
|
266 | 267 | hooks_changegroup_repo_size = v.StringBoolean(if_missing=False) |
|
267 | 268 | hooks_changegroup_push_logger = v.StringBoolean(if_missing=False) |
|
268 |
hooks_ |
|
|
269 | hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False) | |
|
269 | 270 | |
|
270 | 271 | extensions_largefiles = v.StringBoolean(if_missing=False) |
|
271 | 272 | extensions_hgsubversion = v.StringBoolean(if_missing=False) |
@@ -571,11 +571,15 b' class ScmModel(BaseModel):' | |||
|
571 | 571 | if not os.path.isdir(loc): |
|
572 | 572 | os.makedirs(loc) |
|
573 | 573 | |
|
574 | tmpl = pkg_resources.resource_string( | |
|
574 | tmpl_post = pkg_resources.resource_string( | |
|
575 | 575 | 'rhodecode', jn('config', 'post_receive_tmpl.py') |
|
576 | 576 | ) |
|
577 | tmpl_pre = pkg_resources.resource_string( | |
|
578 | 'rhodecode', jn('config', 'pre_receive_tmpl.py') | |
|
579 | ) | |
|
577 | 580 | |
|
578 | _hook_file = jn(loc, 'post-receive') | |
|
581 | for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]: | |
|
582 | _hook_file = jn(loc, '%s-receive' % h_type) | |
|
579 | 583 | _rhodecode_hook = False |
|
580 | 584 | log.debug('Installing git hook in repo %s' % repo) |
|
581 | 585 | if os.path.exists(_hook_file): |
@@ -593,9 +597,12 b' class ScmModel(BaseModel):' | |||
|
593 | 597 | _rhodecode_hook = True |
|
594 | 598 | except: |
|
595 | 599 | log.error(traceback.format_exc()) |
|
600 | else: | |
|
601 | # there is no hook in this dir, so we want to create one | |
|
602 | _rhodecode_hook = True | |
|
596 | 603 | |
|
597 | 604 | if _rhodecode_hook or force_create: |
|
598 | log.debug('writing hook file !') | |
|
605 | log.debug('writing %s hook file !' % h_type) | |
|
599 | 606 | with open(_hook_file, 'wb') as f: |
|
600 | 607 | tmpl = tmpl.replace('_TMPL_', rhodecode.__version__) |
|
601 | 608 | f.write(tmpl) |
@@ -108,6 +108,15 b'' | |||
|
108 | 108 | </div> |
|
109 | 109 | </div> |
|
110 | 110 | <div class="field"> |
|
111 | <div class="label label-checkbox"> | |
|
112 | <label for="enable_locking">${_('Enable locking')}:</label> | |
|
113 | </div> | |
|
114 | <div class="checkboxes"> | |
|
115 | ${h.checkbox('enable_locking',value="True")} | |
|
116 | <span class="help-block">${_('Enable lock-by-pulling on repository.')}</span> | |
|
117 | </div> | |
|
118 | </div> | |
|
119 | <div class="field"> | |
|
111 | 120 | <div class="label"> |
|
112 | 121 | <label for="user">${_('Owner')}:</label> |
|
113 | 122 | </div> |
@@ -196,23 +205,28 b'' | |||
|
196 | 205 | </div> |
|
197 | 206 | <div class="field" style="border:none;color:#888"> |
|
198 | 207 | <ul> |
|
199 |
<li>${_(' |
|
|
208 | <li>${_('All actions made on this repository will be accessible to everyone in public journal')} | |
|
200 | 209 | </li> |
|
201 | 210 | </ul> |
|
202 | 211 | </div> |
|
203 | 212 | </div> |
|
204 | 213 | ${h.end_form()} |
|
205 | 214 | |
|
206 |
<h3>${_(' |
|
|
207 |
${h.form(url('repo', repo_name=c.repo_info.repo_name),method=' |
|
|
215 | <h3>${_('Locking')}</h3> | |
|
216 | ${h.form(url('repo_locking', repo_name=c.repo_info.repo_name),method='put')} | |
|
208 | 217 | <div class="form"> |
|
209 | 218 | <div class="fields"> |
|
210 | ${h.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="ui-btn red",onclick="return confirm('"+_('Confirm to delete this repository')+"');")} | |
|
219 | %if c.repo_info.locked[0]: | |
|
220 | ${h.submit('set_unlock' ,_('Unlock locked repo'),class_="ui-btn",onclick="return confirm('"+_('Confirm to unlock repository')+"');")} | |
|
221 | ${'Locked by %s on %s' % (h.person_by_id(c.repo_info.locked[0]),h.fmt_date(h.time_to_datetime(c.repo_info.locked[1])))} | |
|
222 | %else: | |
|
223 | ${h.submit('set_lock',_('lock repo'),class_="ui-btn",onclick="return confirm('"+_('Confirm to lock repository')+"');")} | |
|
224 | ${_('Repository is not locked')} | |
|
225 | %endif | |
|
211 | 226 | </div> |
|
212 | 227 | <div class="field" style="border:none;color:#888"> |
|
213 | 228 | <ul> |
|
214 | <li>${_('''This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems. | |
|
215 | If you need fully delete it from filesystem please do it manually''')} | |
|
229 | <li>${_('Force locking on repository. Works only when anonymous access is disabled')} | |
|
216 | 230 | </li> |
|
217 | 231 | </ul> |
|
218 | 232 | </div> |
@@ -234,7 +248,21 b'' | |||
|
234 | 248 | </div> |
|
235 | 249 | ${h.end_form()} |
|
236 | 250 | |
|
251 | <h3>${_('Delete')}</h3> | |
|
252 | ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='delete')} | |
|
253 | <div class="form"> | |
|
254 | <div class="fields"> | |
|
255 | ${h.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="ui-btn red",onclick="return confirm('"+_('Confirm to delete this repository')+"');")} | |
|
256 | </div> | |
|
257 | <div class="field" style="border:none;color:#888"> | |
|
258 | <ul> | |
|
259 | <li>${_('''This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems. | |
|
260 | If you need fully delete it from filesystem please do it manually''')} | |
|
261 | </li> | |
|
262 | </ul> | |
|
263 | </div> | |
|
264 | </div> | |
|
265 | ${h.end_form()} | |
|
237 | 266 | </div> |
|
238 | 267 | |
|
239 | ||
|
240 | 268 | </%def> |
@@ -211,8 +211,8 b'' | |||
|
211 | 211 | <label for="hooks_changegroup_push_logger">${_('Log user push commands')}</label> |
|
212 | 212 | </div> |
|
213 | 213 | <div class="checkbox"> |
|
214 |
${h.checkbox('hooks_ |
|
|
215 |
<label for="hooks_ |
|
|
214 | ${h.checkbox('hooks_outgoing_pull_logger','True')} | |
|
215 | <label for="hooks_outgoing_pull_logger">${_('Log user pull commands')}</label> | |
|
216 | 216 | </div> |
|
217 | 217 | </div> |
|
218 | 218 | <div class="input" style="margin-top:10px"> |
General Comments 0
You need to be logged in to leave comments.
Login now