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 | usage/general |
|
22 | usage/general | |
23 | usage/git_support |
|
23 | usage/git_support | |
24 | usage/performance |
|
24 | usage/performance | |
|
25 | usage/locking | |||
25 | usage/statistics |
|
26 | usage/statistics | |
26 | usage/backup |
|
27 | usage/backup | |
27 | usage/debugging |
|
28 | usage/debugging |
@@ -138,7 +138,9 b' def make_map(config):' | |||||
138 | m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*?}", |
|
138 | m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*?}", | |
139 | action="repo_as_fork", conditions=dict(method=["PUT"], |
|
139 | action="repo_as_fork", conditions=dict(method=["PUT"], | |
140 | function=check_repo)) |
|
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 | with rmap.submapper(path_prefix=ADMIN_PREFIX, |
|
144 | with rmap.submapper(path_prefix=ADMIN_PREFIX, | |
143 | controller='admin/repos_groups') as m: |
|
145 | controller='admin/repos_groups') as m: | |
144 | m.connect("repos_groups", "/repos_groups", |
|
146 | m.connect("repos_groups", "/repos_groups", |
@@ -381,6 +381,7 b' class ReposController(BaseController):' | |||||
381 | RepoModel().delete_stats(repo_name) |
|
381 | RepoModel().delete_stats(repo_name) | |
382 | Session().commit() |
|
382 | Session().commit() | |
383 | except Exception, e: |
|
383 | except Exception, e: | |
|
384 | log.error(traceback.format_exc()) | |||
384 | h.flash(_('An error occurred during deletion of repository stats'), |
|
385 | h.flash(_('An error occurred during deletion of repository stats'), | |
385 | category='error') |
|
386 | category='error') | |
386 | return redirect(url('edit_repo', repo_name=repo_name)) |
|
387 | return redirect(url('edit_repo', repo_name=repo_name)) | |
@@ -397,11 +398,32 b' class ReposController(BaseController):' | |||||
397 | ScmModel().mark_for_invalidation(repo_name) |
|
398 | ScmModel().mark_for_invalidation(repo_name) | |
398 | Session().commit() |
|
399 | Session().commit() | |
399 | except Exception, e: |
|
400 | except Exception, e: | |
|
401 | log.error(traceback.format_exc()) | |||
400 | h.flash(_('An error occurred during cache invalidation'), |
|
402 | h.flash(_('An error occurred during cache invalidation'), | |
401 | category='error') |
|
403 | category='error') | |
402 | return redirect(url('edit_repo', repo_name=repo_name)) |
|
404 | return redirect(url('edit_repo', repo_name=repo_name)) | |
403 |
|
405 | |||
404 | @HasPermissionAllDecorator('hg.admin') |
|
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 | def repo_public_journal(self, repo_name): |
|
427 | def repo_public_journal(self, repo_name): | |
406 | """ |
|
428 | """ | |
407 | Set's this repository to be visible in public journal, |
|
429 | Set's this repository to be visible in public journal, |
@@ -807,7 +807,7 b' class HasPermissionAnyMiddleware(object)' | |||||
807 | return self.check_permissions() |
|
807 | return self.check_permissions() | |
808 |
|
808 | |||
809 | def check_permissions(self): |
|
809 | def check_permissions(self): | |
810 |
log.debug('checking |
|
810 | log.debug('checking VCS protocol ' | |
811 | 'permissions %s for user:%s repository:%s', self.user_perms, |
|
811 | 'permissions %s for user:%s repository:%s', self.user_perms, | |
812 | self.username, self.repo_name) |
|
812 | self.username, self.repo_name) | |
813 | if self.required_perms.intersection(self.user_perms): |
|
813 | if self.required_perms.intersection(self.user_perms): |
@@ -8,6 +8,7 b' import traceback' | |||||
8 |
|
8 | |||
9 | from paste.auth.basic import AuthBasicAuthenticator |
|
9 | from paste.auth.basic import AuthBasicAuthenticator | |
10 | from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden |
|
10 | from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden | |
|
11 | from webob.exc import HTTPClientError | |||
11 | from paste.httpheaders import WWW_AUTHENTICATE |
|
12 | from paste.httpheaders import WWW_AUTHENTICATE | |
12 |
|
13 | |||
13 | from pylons import config, tmpl_context as c, request, session, url |
|
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 | from rhodecode import __version__, BACKENDS |
|
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 | from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\ |
|
23 | from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\ | |
22 | HasPermissionAnyMiddleware, CookieStoreWrapper |
|
24 | HasPermissionAnyMiddleware, CookieStoreWrapper | |
23 | from rhodecode.lib.utils import get_repo_slug, invalidate_cache |
|
25 | from rhodecode.lib.utils import get_repo_slug, invalidate_cache | |
24 | from rhodecode.model import meta |
|
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 | from rhodecode.model.notification import NotificationModel |
|
29 | from rhodecode.model.notification import NotificationModel | |
28 | from rhodecode.model.scm import ScmModel |
|
30 | from rhodecode.model.scm import ScmModel | |
|
31 | from rhodecode.model.meta import Session | |||
29 |
|
32 | |||
30 | log = logging.getLogger(__name__) |
|
33 | log = logging.getLogger(__name__) | |
31 |
|
34 | |||
@@ -159,6 +162,49 b' class BaseVCSController(object):' | |||||
159 | return False |
|
162 | return False | |
160 | return True |
|
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 | def __call__(self, environ, start_response): |
|
208 | def __call__(self, environ, start_response): | |
163 | start = time.time() |
|
209 | start = time.time() | |
164 | try: |
|
210 | try: |
@@ -307,37 +307,47 b' class DbManage(object):' | |||||
307 | hooks1.ui_key = hooks1_key |
|
307 | hooks1.ui_key = hooks1_key | |
308 | hooks1.ui_value = 'hg update >&2' |
|
308 | hooks1.ui_value = 'hg update >&2' | |
309 | hooks1.ui_active = False |
|
309 | hooks1.ui_active = False | |
|
310 | self.sa.add(hooks1) | |||
310 |
|
311 | |||
311 | hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE |
|
312 | hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE | |
312 | hooks2_ = self.sa.query(RhodeCodeUi)\ |
|
313 | hooks2_ = self.sa.query(RhodeCodeUi)\ | |
313 | .filter(RhodeCodeUi.ui_key == hooks2_key).scalar() |
|
314 | .filter(RhodeCodeUi.ui_key == hooks2_key).scalar() | |
314 |
|
||||
315 | hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_ |
|
315 | hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_ | |
316 | hooks2.ui_section = 'hooks' |
|
316 | hooks2.ui_section = 'hooks' | |
317 | hooks2.ui_key = hooks2_key |
|
317 | hooks2.ui_key = hooks2_key | |
318 | hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size' |
|
318 | hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size' | |
|
319 | self.sa.add(hooks2) | |||
319 |
|
320 | |||
320 | hooks3 = RhodeCodeUi() |
|
321 | hooks3 = RhodeCodeUi() | |
321 | hooks3.ui_section = 'hooks' |
|
322 | hooks3.ui_section = 'hooks' | |
322 | hooks3.ui_key = RhodeCodeUi.HOOK_PUSH |
|
323 | hooks3.ui_key = RhodeCodeUi.HOOK_PUSH | |
323 | hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action' |
|
324 | hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action' | |
|
325 | self.sa.add(hooks3) | |||
324 |
|
326 | |||
325 | hooks4 = RhodeCodeUi() |
|
327 | hooks4 = RhodeCodeUi() | |
326 | hooks4.ui_section = 'hooks' |
|
328 | hooks4.ui_section = 'hooks' | |
327 |
hooks4.ui_key = RhodeCodeUi.HOOK_P |
|
329 | hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH | |
328 |
hooks4.ui_value = 'python:rhodecode.lib.hooks. |
|
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 |
|
333 | hooks5 = RhodeCodeUi() | |
331 | dotencode_disable = RhodeCodeUi() |
|
334 | hooks5.ui_section = 'hooks' | |
332 | dotencode_disable.ui_section = 'format' |
|
335 | hooks5.ui_key = RhodeCodeUi.HOOK_PULL | |
333 | dotencode_disable.ui_key = 'dotencode' |
|
336 | hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action' | |
334 | dotencode_disable.ui_value = 'false' |
|
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 | # enable largefiles |
|
345 | # enable largefiles | |
337 | largefiles = RhodeCodeUi() |
|
346 | largefiles = RhodeCodeUi() | |
338 | largefiles.ui_section = 'extensions' |
|
347 | largefiles.ui_section = 'extensions' | |
339 | largefiles.ui_key = 'largefiles' |
|
348 | largefiles.ui_key = 'largefiles' | |
340 | largefiles.ui_value = '' |
|
349 | largefiles.ui_value = '' | |
|
350 | self.sa.add(largefiles) | |||
341 |
|
351 | |||
342 | # enable hgsubversion disabled by default |
|
352 | # enable hgsubversion disabled by default | |
343 | hgsubversion = RhodeCodeUi() |
|
353 | hgsubversion = RhodeCodeUi() | |
@@ -345,6 +355,7 b' class DbManage(object):' | |||||
345 | hgsubversion.ui_key = 'hgsubversion' |
|
355 | hgsubversion.ui_key = 'hgsubversion' | |
346 | hgsubversion.ui_value = '' |
|
356 | hgsubversion.ui_value = '' | |
347 | hgsubversion.ui_active = False |
|
357 | hgsubversion.ui_active = False | |
|
358 | self.sa.add(hgsubversion) | |||
348 |
|
359 | |||
349 | # enable hggit disabled by default |
|
360 | # enable hggit disabled by default | |
350 | hggit = RhodeCodeUi() |
|
361 | hggit = RhodeCodeUi() | |
@@ -352,13 +363,6 b' class DbManage(object):' | |||||
352 | hggit.ui_key = 'hggit' |
|
363 | hggit.ui_key = 'hggit' | |
353 | hggit.ui_value = '' |
|
364 | hggit.ui_value = '' | |
354 | hggit.ui_active = False |
|
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 | self.sa.add(hggit) |
|
366 | self.sa.add(hggit) | |
363 |
|
367 | |||
364 | def create_ldap_options(self, skip_existing=False): |
|
368 | def create_ldap_options(self, skip_existing=False): | |
@@ -461,6 +465,11 b' class DbManage(object):' | |||||
461 | paths.ui_key = '/' |
|
465 | paths.ui_key = '/' | |
462 | paths.ui_value = path |
|
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 | sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication') |
|
473 | sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication') | |
465 | sett2 = RhodeCodeSetting('title', 'RhodeCode') |
|
474 | sett2 = RhodeCodeSetting('title', 'RhodeCode') | |
466 | sett3 = RhodeCodeSetting('ga_code', '') |
|
475 | sett3 = RhodeCodeSetting('ga_code', '') |
@@ -23,6 +23,8 b'' | |||||
23 | # You should have received a copy of the GNU General Public License |
|
23 | # You should have received a copy of the GNU General Public License | |
24 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
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 | class LdapUsernameError(Exception): |
|
29 | class LdapUsernameError(Exception): | |
28 | pass |
|
30 | pass | |
@@ -53,4 +55,17 b' class UsersGroupsAssignedException(Excep' | |||||
53 |
|
55 | |||
54 |
|
56 | |||
55 | class StatusChangeOnClosedPullRequestError(Exception): |
|
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 | from rhodecode.lib.annotate import annotate_highlight |
|
41 | from rhodecode.lib.annotate import annotate_highlight | |
42 | from rhodecode.lib.utils import repo_name_slug |
|
42 | from rhodecode.lib.utils import repo_name_slug | |
43 | from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ |
|
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 | from rhodecode.lib.markup_renderer import MarkupRenderer |
|
45 | from rhodecode.lib.markup_renderer import MarkupRenderer | |
46 | from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError |
|
46 | from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError | |
47 | from rhodecode.lib.vcs.backends.base import BaseChangeset |
|
47 | from rhodecode.lib.vcs.backends.base import BaseChangeset | |
@@ -439,6 +439,19 b' def person(author):' | |||||
439 | return _author |
|
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 | def desc_stylize(value): |
|
455 | def desc_stylize(value): | |
443 | """ |
|
456 | """ | |
444 | converts tags from value into html equivalent |
|
457 | converts tags from value into html equivalent |
@@ -34,6 +34,9 b' from rhodecode.lib import helpers as h' | |||||
34 | from rhodecode.lib.utils import action_logger |
|
34 | from rhodecode.lib.utils import action_logger | |
35 | from rhodecode.lib.vcs.backends.base import EmptyChangeset |
|
35 | from rhodecode.lib.vcs.backends.base import EmptyChangeset | |
36 | from rhodecode.lib.compat import json |
|
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 | def _get_scm_size(alias, root_path): |
|
42 | def _get_scm_size(alias, root_path): | |
@@ -84,6 +87,59 b' def repo_size(ui, repo, hooktype=None, *' | |||||
84 | sys.stdout.write(msg) |
|
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 | def log_pull_action(ui, repo, **kwargs): |
|
143 | def log_pull_action(ui, repo, **kwargs): | |
88 | """ |
|
144 | """ | |
89 | Logs user last pull action |
|
145 | Logs user last pull action | |
@@ -100,15 +156,17 b' def log_pull_action(ui, repo, **kwargs):' | |||||
100 | username = extras['username'] |
|
156 | username = extras['username'] | |
101 | repository = extras['repository'] |
|
157 | repository = extras['repository'] | |
102 | scm = extras['scm'] |
|
158 | scm = extras['scm'] | |
|
159 | make_lock = extras['make_lock'] | |||
103 | elif 'username' in rc_extras: |
|
160 | elif 'username' in rc_extras: | |
104 | username = rc_extras['username'] |
|
161 | username = rc_extras['username'] | |
105 | repository = rc_extras['repository'] |
|
162 | repository = rc_extras['repository'] | |
106 | scm = rc_extras['scm'] |
|
163 | scm = rc_extras['scm'] | |
|
164 | make_lock = rc_extras['make_lock'] | |||
107 | else: |
|
165 | else: | |
108 | raise Exception('Missing data in repo.ui and os.environ') |
|
166 | raise Exception('Missing data in repo.ui and os.environ') | |
109 |
|
167 | user = User.get_by_username(username) | ||
110 | action = 'pull' |
|
168 | action = 'pull' | |
111 |
action_logger(user |
|
169 | action_logger(user, action, repository, extras['ip'], commit=True) | |
112 | # extension hook call |
|
170 | # extension hook call | |
113 | from rhodecode import EXTENSIONS |
|
171 | from rhodecode import EXTENSIONS | |
114 | callback = getattr(EXTENSIONS, 'PULL_HOOK', None) |
|
172 | callback = getattr(EXTENSIONS, 'PULL_HOOK', None) | |
@@ -117,6 +175,12 b' def log_pull_action(ui, repo, **kwargs):' | |||||
117 | kw = {} |
|
175 | kw = {} | |
118 | kw.update(extras) |
|
176 | kw.update(extras) | |
119 | callback(**kw) |
|
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 | return 0 |
|
184 | return 0 | |
121 |
|
185 | |||
122 |
|
186 | |||
@@ -138,10 +202,12 b' def log_push_action(ui, repo, **kwargs):' | |||||
138 | username = extras['username'] |
|
202 | username = extras['username'] | |
139 | repository = extras['repository'] |
|
203 | repository = extras['repository'] | |
140 | scm = extras['scm'] |
|
204 | scm = extras['scm'] | |
|
205 | make_lock = extras['make_lock'] | |||
141 | elif 'username' in rc_extras: |
|
206 | elif 'username' in rc_extras: | |
142 | username = rc_extras['username'] |
|
207 | username = rc_extras['username'] | |
143 | repository = rc_extras['repository'] |
|
208 | repository = rc_extras['repository'] | |
144 | scm = rc_extras['scm'] |
|
209 | scm = rc_extras['scm'] | |
|
210 | make_lock = rc_extras['make_lock'] | |||
145 | else: |
|
211 | else: | |
146 | raise Exception('Missing data in repo.ui and os.environ') |
|
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 | kw = {'pushed_revs': revs} |
|
245 | kw = {'pushed_revs': revs} | |
180 | kw.update(extras) |
|
246 | kw.update(extras) | |
181 | callback(**kw) |
|
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 | return 0 |
|
254 | return 0 | |
183 |
|
255 | |||
184 |
|
256 | |||
@@ -219,8 +291,13 b' def log_create_repository(repository_dic' | |||||
219 |
|
291 | |||
220 | return 0 |
|
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 | A really hacky method that is runned by git post-receive hook and logs |
|
302 | A really hacky method that is runned by git post-receive hook and logs | |
226 | an push action together with pushed revisions. It's executed by subprocess |
|
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 | from rhodecode.model import init_model |
|
317 | from rhodecode.model import init_model | |
241 | from rhodecode.model.db import RhodeCodeUi |
|
318 | from rhodecode.model.db import RhodeCodeUi | |
242 | from rhodecode.lib.utils import make_ui |
|
319 | from rhodecode.lib.utils import make_ui | |
243 | from rhodecode.model.db import Repository |
|
|||
244 |
|
320 | |||
245 | path, ini_name = os.path.split(env['RHODECODE_CONFIG_FILE']) |
|
321 | path, ini_name = os.path.split(env['RHODECODE_CONFIG_FILE']) | |
246 | conf = appconfig('config:%s' % ini_name, relative_to=path) |
|
322 | conf = appconfig('config:%s' % ini_name, relative_to=path) | |
@@ -255,21 +331,19 b' def handle_git_post_receive(repo_path, r' | |||||
255 | repo_path = repo_path[:-4] |
|
331 | repo_path = repo_path[:-4] | |
256 | repo = Repository.get_by_full_path(repo_path) |
|
332 | repo = Repository.get_by_full_path(repo_path) | |
257 | _hooks = dict(baseui.configitems('hooks')) or {} |
|
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 = { |
|
335 | extras = json.loads(env['RHODECODE_EXTRAS']) | |
262 | 'username': env['RHODECODE_USER'], |
|
|||
263 | 'repository': repo.repo_name, |
|
|||
264 | 'scm': 'git', |
|
|||
265 | 'action': 'push', |
|
|||
266 | 'ip': env['RHODECODE_CONFIG_IP'], |
|
|||
267 | } |
|
|||
268 |
|
|
336 | for k, v in extras.items(): | |
269 |
|
|
337 | baseui.setconfig('rhodecode_extras', k, v) | |
270 |
|
|
338 | repo = repo.scm_instance | |
271 |
|
|
339 | repo.ui = baseui | |
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 | rev_data = [] |
|
347 | rev_data = [] | |
274 | for l in revs: |
|
348 | for l in revs: | |
275 | old_rev, new_rev, ref = l.split(' ') |
|
349 | old_rev, new_rev, ref = l.split(' ') |
@@ -41,7 +41,7 b' class GitRepository(object):' | |||||
41 | git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs']) |
|
41 | git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs']) | |
42 | commands = ['git-upload-pack', 'git-receive-pack'] |
|
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 | files = set([f.lower() for f in os.listdir(content_path)]) |
|
45 | files = set([f.lower() for f in os.listdir(content_path)]) | |
46 | if not (self.git_folder_signature.intersection(files) |
|
46 | if not (self.git_folder_signature.intersection(files) | |
47 | == self.git_folder_signature): |
|
47 | == self.git_folder_signature): | |
@@ -50,7 +50,7 b' class GitRepository(object):' | |||||
50 | self.valid_accepts = ['application/x-%s-result' % |
|
50 | self.valid_accepts = ['application/x-%s-result' % | |
51 | c for c in self.commands] |
|
51 | c for c in self.commands] | |
52 | self.repo_name = repo_name |
|
52 | self.repo_name = repo_name | |
53 |
self. |
|
53 | self.extras = extras | |
54 |
|
54 | |||
55 | def _get_fixedpath(self, path): |
|
55 | def _get_fixedpath(self, path): | |
56 | """ |
|
56 | """ | |
@@ -67,7 +67,7 b' class GitRepository(object):' | |||||
67 | HTTP /info/refs request. |
|
67 | HTTP /info/refs request. | |
68 | """ |
|
68 | """ | |
69 |
|
69 | |||
70 |
git_command = request.GET |
|
70 | git_command = request.GET.get('service') | |
71 | if git_command not in self.commands: |
|
71 | if git_command not in self.commands: | |
72 | log.debug('command %s not allowed' % git_command) |
|
72 | log.debug('command %s not allowed' % git_command) | |
73 | return exc.HTTPMethodNotAllowed() |
|
73 | return exc.HTTPMethodNotAllowed() | |
@@ -119,9 +119,8 b' class GitRepository(object):' | |||||
119 | try: |
|
119 | try: | |
120 | gitenv = os.environ |
|
120 | gitenv = os.environ | |
121 | from rhodecode import CONFIG |
|
121 | from rhodecode import CONFIG | |
122 |
from rhodecode.lib. |
|
122 | from rhodecode.lib.compat import json | |
123 |
gitenv['RHODECODE_ |
|
123 | gitenv['RHODECODE_EXTRAS'] = json.dumps(self.extras) | |
124 | gitenv['RHODECODE_CONFIG_IP'] = _get_ip_addr(environ) |
|
|||
125 | # forget all configs |
|
124 | # forget all configs | |
126 | gitenv['GIT_CONFIG_NOGLOBAL'] = '1' |
|
125 | gitenv['GIT_CONFIG_NOGLOBAL'] = '1' | |
127 | # we need current .ini file used to later initialize rhodecode |
|
126 | # we need current .ini file used to later initialize rhodecode | |
@@ -174,7 +173,7 b' class GitRepository(object):' | |||||
174 |
|
173 | |||
175 | class GitDirectory(object): |
|
174 | class GitDirectory(object): | |
176 |
|
175 | |||
177 |
def __init__(self, repo_root, repo_name, |
|
176 | def __init__(self, repo_root, repo_name, extras): | |
178 | repo_location = os.path.join(repo_root, repo_name) |
|
177 | repo_location = os.path.join(repo_root, repo_name) | |
179 | if not os.path.isdir(repo_location): |
|
178 | if not os.path.isdir(repo_location): | |
180 | raise OSError(repo_location) |
|
179 | raise OSError(repo_location) | |
@@ -182,12 +181,12 b' class GitDirectory(object):' | |||||
182 | self.content_path = repo_location |
|
181 | self.content_path = repo_location | |
183 | self.repo_name = repo_name |
|
182 | self.repo_name = repo_name | |
184 | self.repo_location = repo_location |
|
183 | self.repo_location = repo_location | |
185 |
self. |
|
184 | self.extras = extras | |
186 |
|
185 | |||
187 | def __call__(self, environ, start_response): |
|
186 | def __call__(self, environ, start_response): | |
188 | content_path = self.content_path |
|
187 | content_path = self.content_path | |
189 | try: |
|
188 | try: | |
190 |
app = GitRepository(self.repo_name, content_path, self. |
|
189 | app = GitRepository(self.repo_name, content_path, self.extras) | |
191 | except (AssertionError, OSError): |
|
190 | except (AssertionError, OSError): | |
192 | if os.path.isdir(os.path.join(content_path, '.git')): |
|
191 | if os.path.isdir(os.path.join(content_path, '.git')): | |
193 | app = GitRepository(self.repo_name, |
|
192 | app = GitRepository(self.repo_name, | |
@@ -198,5 +197,5 b' class GitDirectory(object):' | |||||
198 | return app(environ, start_response) |
|
197 | return app(environ, start_response) | |
199 |
|
198 | |||
200 |
|
199 | |||
201 |
def make_wsgi_app(repo_name, repo_root, |
|
200 | def make_wsgi_app(repo_name, repo_root, extras): | |
202 |
return GitDirectory(repo_root, repo_name, |
|
201 | return GitDirectory(repo_root, repo_name, extras) |
@@ -31,6 +31,8 b' import traceback' | |||||
31 |
|
31 | |||
32 | from dulwich import server as dulserver |
|
32 | from dulwich import server as dulserver | |
33 | from dulwich.web import LimitedInputFilter, GunzipFilter |
|
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 | class SimpleGitUploadPackHandler(dulserver.UploadPackHandler): |
|
38 | class SimpleGitUploadPackHandler(dulserver.UploadPackHandler): | |
@@ -102,11 +104,11 b' def is_git(environ):' | |||||
102 | class SimpleGit(BaseVCSController): |
|
104 | class SimpleGit(BaseVCSController): | |
103 |
|
105 | |||
104 | def _handle_request(self, environ, start_response): |
|
106 | def _handle_request(self, environ, start_response): | |
105 |
|
||||
106 | if not is_git(environ): |
|
107 | if not is_git(environ): | |
107 | return self.application(environ, start_response) |
|
108 | return self.application(environ, start_response) | |
108 | if not self._check_ssl(environ, start_response): |
|
109 | if not self._check_ssl(environ, start_response): | |
109 | return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response) |
|
110 | return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response) | |
|
111 | ||||
110 | ipaddr = self._get_ip_addr(environ) |
|
112 | ipaddr = self._get_ip_addr(environ) | |
111 | username = None |
|
113 | username = None | |
112 | self._git_first_op = False |
|
114 | self._git_first_op = False | |
@@ -184,21 +186,39 b' class SimpleGit(BaseVCSController):' | |||||
184 | if perm is not True: |
|
186 | if perm is not True: | |
185 | return HTTPForbidden()(environ, start_response) |
|
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 | extras = { |
|
191 | extras = { | |
188 | 'ip': ipaddr, |
|
192 | 'ip': ipaddr, | |
189 | 'username': username, |
|
193 | 'username': username, | |
190 | 'action': action, |
|
194 | 'action': action, | |
191 | 'repository': repo_name, |
|
195 | 'repository': repo_name, | |
192 | 'scm': 'git', |
|
196 | 'scm': 'git', | |
|
197 | 'make_lock': None, | |||
|
198 | 'locked_by': [None, None] | |||
193 | } |
|
199 | } | |
194 | # set the environ variables for this request |
|
200 | ||
195 | os.environ['RC_SCM_DATA'] = json.dumps(extras) |
|
|||
196 | #=================================================================== |
|
201 | #=================================================================== | |
197 | # GIT REQUEST HANDLING |
|
202 | # GIT REQUEST HANDLING | |
198 | #=================================================================== |
|
203 | #=================================================================== | |
199 | repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) |
|
204 | repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) | |
200 | log.debug('Repository path is %s' % repo_path) |
|
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 | baseui = make_ui('db') |
|
222 | baseui = make_ui('db') | |
203 | self.__inject_extras(repo_path, baseui, extras) |
|
223 | self.__inject_extras(repo_path, baseui, extras) | |
204 |
|
224 | |||
@@ -209,13 +229,16 b' class SimpleGit(BaseVCSController):' | |||||
209 | self._handle_githooks(repo_name, action, baseui, environ) |
|
229 | self._handle_githooks(repo_name, action, baseui, environ) | |
210 |
|
230 | |||
211 | log.info('%s action on GIT repo "%s"' % (action, repo_name)) |
|
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 | return app(environ, start_response) |
|
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 | except Exception: |
|
237 | except Exception: | |
215 | log.error(traceback.format_exc()) |
|
238 | log.error(traceback.format_exc()) | |
216 | return HTTPInternalServerError()(environ, start_response) |
|
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 | Make an wsgi application using dulserver |
|
243 | Make an wsgi application using dulserver | |
221 |
|
244 | |||
@@ -227,7 +250,7 b' class SimpleGit(BaseVCSController):' | |||||
227 | app = make_wsgi_app( |
|
250 | app = make_wsgi_app( | |
228 | repo_root=safe_str(self.basepath), |
|
251 | repo_root=safe_str(self.basepath), | |
229 | repo_name=repo_name, |
|
252 | repo_name=repo_name, | |
230 |
|
|
253 | extras=extras, | |
231 | ) |
|
254 | ) | |
232 | app = GunzipFilter(LimitedInputFilter(app)) |
|
255 | app = GunzipFilter(LimitedInputFilter(app)) | |
233 | return app |
|
256 | return app | |
@@ -279,6 +302,7 b' class SimpleGit(BaseVCSController):' | |||||
279 | """ |
|
302 | """ | |
280 | from rhodecode.lib.hooks import log_pull_action |
|
303 | from rhodecode.lib.hooks import log_pull_action | |
281 | service = environ['QUERY_STRING'].split('=') |
|
304 | service = environ['QUERY_STRING'].split('=') | |
|
305 | ||||
282 | if len(service) < 2: |
|
306 | if len(service) < 2: | |
283 | return |
|
307 | return | |
284 |
|
308 | |||
@@ -288,6 +312,9 b' class SimpleGit(BaseVCSController):' | |||||
288 | _repo._repo.ui = baseui |
|
312 | _repo._repo.ui = baseui | |
289 |
|
313 | |||
290 | _hooks = dict(baseui.configitems('hooks')) or {} |
|
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 | if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL): |
|
318 | if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL): | |
292 | log_pull_action(ui=baseui, repo=_repo._repo) |
|
319 | log_pull_action(ui=baseui, repo=_repo._repo) | |
293 |
|
320 |
@@ -42,6 +42,7 b' from rhodecode.lib.auth import get_conta' | |||||
42 | from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections |
|
42 | from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections | |
43 | from rhodecode.lib.compat import json |
|
43 | from rhodecode.lib.compat import json | |
44 | from rhodecode.model.db import User |
|
44 | from rhodecode.model.db import User | |
|
45 | from rhodecode.lib.exceptions import HTTPLockedRC | |||
45 |
|
46 | |||
46 |
|
47 | |||
47 | log = logging.getLogger(__name__) |
|
48 | log = logging.getLogger(__name__) | |
@@ -157,15 +158,31 b' class SimpleHg(BaseVCSController):' | |||||
157 | 'action': action, |
|
158 | 'action': action, | |
158 | 'repository': repo_name, |
|
159 | 'repository': repo_name, | |
159 | 'scm': 'hg', |
|
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 | # MERCURIAL REQUEST HANDLING |
|
165 | # MERCURIAL REQUEST HANDLING | |
165 | #====================================================================== |
|
166 | #====================================================================== | |
166 | repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) |
|
167 | repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) | |
167 | log.debug('Repository path is %s' % repo_path) |
|
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 | baseui = make_ui('db') |
|
186 | baseui = make_ui('db') | |
170 | self.__inject_extras(repo_path, baseui, extras) |
|
187 | self.__inject_extras(repo_path, baseui, extras) | |
171 |
|
188 | |||
@@ -179,6 +196,9 b' class SimpleHg(BaseVCSController):' | |||||
179 | except RepoError, e: |
|
196 | except RepoError, e: | |
180 | if str(e).find('not found') != -1: |
|
197 | if str(e).find('not found') != -1: | |
181 | return HTTPNotFound()(environ, start_response) |
|
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 | except Exception: |
|
202 | except Exception: | |
183 | log.error(traceback.format_exc()) |
|
203 | log.error(traceback.format_exc()) | |
184 | return HTTPInternalServerError()(environ, start_response) |
|
204 | return HTTPInternalServerError()(environ, start_response) |
@@ -25,7 +25,7 b'' | |||||
25 |
|
25 | |||
26 | import re |
|
26 | import re | |
27 | import time |
|
27 | import time | |
28 |
|
|
28 | import datetime | |
29 | from pylons.i18n.translation import _, ungettext |
|
29 | from pylons.i18n.translation import _, ungettext | |
30 | from rhodecode.lib.vcs.utils.lazy import LazyProperty |
|
30 | from rhodecode.lib.vcs.utils.lazy import LazyProperty | |
31 |
|
31 | |||
@@ -300,7 +300,7 b' def age(prevdate):' | |||||
300 | deltas = {} |
|
300 | deltas = {} | |
301 |
|
301 | |||
302 | # Get date parts deltas |
|
302 | # Get date parts deltas | |
303 | now = datetime.now() |
|
303 | now = datetime.datetime.now() | |
304 | for part in order: |
|
304 | for part in order: | |
305 | deltas[part] = getattr(now, part) - getattr(prevdate, part) |
|
305 | deltas[part] = getattr(now, part) - getattr(prevdate, part) | |
306 |
|
306 | |||
@@ -435,6 +435,15 b' def datetime_to_time(dt):' | |||||
435 | return time.mktime(dt.timetuple()) |
|
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 | MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})' |
|
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 | import datetime |
|
28 | import datetime | |
29 | import traceback |
|
29 | import traceback | |
30 | import hashlib |
|
30 | import hashlib | |
|
31 | import time | |||
31 | from collections import defaultdict |
|
32 | from collections import defaultdict | |
32 |
|
33 | |||
33 | from sqlalchemy import * |
|
34 | from sqlalchemy import * | |
@@ -232,7 +233,9 b' class RhodeCodeUi(Base, BaseModel):' | |||||
232 | HOOK_UPDATE = 'changegroup.update' |
|
233 | HOOK_UPDATE = 'changegroup.update' | |
233 | HOOK_REPO_SIZE = 'changegroup.repo_size' |
|
234 | HOOK_REPO_SIZE = 'changegroup.repo_size' | |
234 | HOOK_PUSH = 'changegroup.push_logger' |
|
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 | ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
240 | ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
238 | ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) |
|
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 | @classmethod |
|
250 | @classmethod | |
248 | def get_builtin_hooks(cls): |
|
251 | def get_builtin_hooks(cls): | |
249 | q = cls.query() |
|
252 | q = cls.query() | |
250 | q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, |
|
253 | q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE, | |
251 |
cls.HOOK_ |
|
254 | cls.HOOK_PUSH, cls.HOOK_PRE_PUSH, | |
252 |
cls.HOOK_PU |
|
255 | cls.HOOK_PULL, cls.HOOK_PRE_PULL])) | |
253 | return q.all() |
|
256 | return q.all() | |
254 |
|
257 | |||
255 | @classmethod |
|
258 | @classmethod | |
256 | def get_custom_hooks(cls): |
|
259 | def get_custom_hooks(cls): | |
257 | q = cls.query() |
|
260 | q = cls.query() | |
258 | q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, |
|
261 | q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE, | |
259 |
cls.HOOK_ |
|
262 | cls.HOOK_PUSH, cls.HOOK_PRE_PUSH, | |
260 |
cls.HOOK_PU |
|
263 | cls.HOOK_PULL, cls.HOOK_PRE_PULL])) | |
261 | q = q.filter(cls.ui_section == 'hooks') |
|
264 | q = q.filter(cls.ui_section == 'hooks') | |
262 | return q.all() |
|
265 | return q.all() | |
263 |
|
266 | |||
@@ -280,9 +283,13 b' class User(Base, BaseModel):' | |||||
280 | __tablename__ = 'users' |
|
283 | __tablename__ = 'users' | |
281 | __table_args__ = ( |
|
284 | __table_args__ = ( | |
282 | UniqueConstraint('username'), UniqueConstraint('email'), |
|
285 | UniqueConstraint('username'), UniqueConstraint('email'), | |
|
286 | Index('u_username_idx', 'username'), | |||
|
287 | Index('u_email_idx', 'email'), | |||
283 | {'extend_existing': True, 'mysql_engine': 'InnoDB', |
|
288 | {'extend_existing': True, 'mysql_engine': 'InnoDB', | |
284 | 'mysql_charset': 'utf8'} |
|
289 | 'mysql_charset': 'utf8'} | |
285 | ) |
|
290 | ) | |
|
291 | DEFAULT_USER = 'default' | |||
|
292 | ||||
286 | user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
293 | user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
287 | username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) |
|
294 | username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) | |
288 | password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) |
|
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 | __tablename__ = 'repositories' |
|
579 | __tablename__ = 'repositories' | |
573 | __table_args__ = ( |
|
580 | __table_args__ = ( | |
574 | UniqueConstraint('repo_name'), |
|
581 | UniqueConstraint('repo_name'), | |
|
582 | Index('r_repo_name_idx', 'repo_name'), | |||
575 | {'extend_existing': True, 'mysql_engine': 'InnoDB', |
|
583 | {'extend_existing': True, 'mysql_engine': 'InnoDB', | |
576 | 'mysql_charset': 'utf8'}, |
|
584 | 'mysql_charset': 'utf8'}, | |
577 | ) |
|
585 | ) | |
@@ -587,6 +595,8 b' class Repository(Base, BaseModel):' | |||||
587 | description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) |
|
595 | description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) | |
588 | created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) |
|
596 | created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) | |
589 | landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None) |
|
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 | fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None) |
|
601 | fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None) | |
592 | group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None) |
|
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 | return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, |
|
627 | return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, | |
618 | self.repo_name) |
|
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 | @classmethod |
|
645 | @classmethod | |
621 | def url_sep(cls): |
|
646 | def url_sep(cls): | |
622 | return URL_SEP |
|
647 | return URL_SEP | |
@@ -793,6 +818,18 b' class Repository(Base, BaseModel):' | |||||
793 |
|
818 | |||
794 | return data |
|
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 | # SCM PROPERTIES |
|
834 | # SCM PROPERTIES | |
798 | #========================================================================== |
|
835 | #========================================================================== |
@@ -182,6 +182,7 b' def RepoForm(edit=False, old_data={}, su' | |||||
182 | private = v.StringBoolean(if_missing=False) |
|
182 | private = v.StringBoolean(if_missing=False) | |
183 | enable_statistics = v.StringBoolean(if_missing=False) |
|
183 | enable_statistics = v.StringBoolean(if_missing=False) | |
184 | enable_downloads = v.StringBoolean(if_missing=False) |
|
184 | enable_downloads = v.StringBoolean(if_missing=False) | |
|
185 | enable_locking = v.StringBoolean(if_missing=False) | |||
185 | landing_rev = v.OneOf(landing_revs, hideList=True) |
|
186 | landing_rev = v.OneOf(landing_revs, hideList=True) | |
186 |
|
187 | |||
187 | if edit: |
|
188 | if edit: | |
@@ -265,7 +266,7 b' def ApplicationUiSettingsForm():' | |||||
265 | hooks_changegroup_update = v.StringBoolean(if_missing=False) |
|
266 | hooks_changegroup_update = v.StringBoolean(if_missing=False) | |
266 | hooks_changegroup_repo_size = v.StringBoolean(if_missing=False) |
|
267 | hooks_changegroup_repo_size = v.StringBoolean(if_missing=False) | |
267 | hooks_changegroup_push_logger = v.StringBoolean(if_missing=False) |
|
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 | extensions_largefiles = v.StringBoolean(if_missing=False) |
|
271 | extensions_largefiles = v.StringBoolean(if_missing=False) | |
271 | extensions_hgsubversion = v.StringBoolean(if_missing=False) |
|
272 | extensions_hgsubversion = v.StringBoolean(if_missing=False) |
@@ -571,11 +571,15 b' class ScmModel(BaseModel):' | |||||
571 | if not os.path.isdir(loc): |
|
571 | if not os.path.isdir(loc): | |
572 | os.makedirs(loc) |
|
572 | os.makedirs(loc) | |
573 |
|
573 | |||
574 | tmpl = pkg_resources.resource_string( |
|
574 | tmpl_post = pkg_resources.resource_string( | |
575 | 'rhodecode', jn('config', 'post_receive_tmpl.py') |
|
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 | _rhodecode_hook = False |
|
583 | _rhodecode_hook = False | |
580 | log.debug('Installing git hook in repo %s' % repo) |
|
584 | log.debug('Installing git hook in repo %s' % repo) | |
581 | if os.path.exists(_hook_file): |
|
585 | if os.path.exists(_hook_file): | |
@@ -593,9 +597,12 b' class ScmModel(BaseModel):' | |||||
593 | _rhodecode_hook = True |
|
597 | _rhodecode_hook = True | |
594 | except: |
|
598 | except: | |
595 | log.error(traceback.format_exc()) |
|
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 | if _rhodecode_hook or force_create: |
|
604 | if _rhodecode_hook or force_create: | |
598 | log.debug('writing hook file !') |
|
605 | log.debug('writing %s hook file !' % h_type) | |
599 | with open(_hook_file, 'wb') as f: |
|
606 | with open(_hook_file, 'wb') as f: | |
600 | tmpl = tmpl.replace('_TMPL_', rhodecode.__version__) |
|
607 | tmpl = tmpl.replace('_TMPL_', rhodecode.__version__) | |
601 | f.write(tmpl) |
|
608 | f.write(tmpl) |
@@ -108,6 +108,15 b'' | |||||
108 | </div> |
|
108 | </div> | |
109 | </div> |
|
109 | </div> | |
110 | <div class="field"> |
|
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 | <div class="label"> |
|
120 | <div class="label"> | |
112 | <label for="user">${_('Owner')}:</label> |
|
121 | <label for="user">${_('Owner')}:</label> | |
113 | </div> |
|
122 | </div> | |
@@ -196,23 +205,28 b'' | |||||
196 | </div> |
|
205 | </div> | |
197 | <div class="field" style="border:none;color:#888"> |
|
206 | <div class="field" style="border:none;color:#888"> | |
198 | <ul> |
|
207 | <ul> | |
199 |
<li>${_(' |
|
208 | <li>${_('All actions made on this repository will be accessible to everyone in public journal')} | |
200 | </li> |
|
209 | </li> | |
201 | </ul> |
|
210 | </ul> | |
202 | </div> |
|
211 | </div> | |
203 | </div> |
|
212 | </div> | |
204 | ${h.end_form()} |
|
213 | ${h.end_form()} | |
205 |
|
214 | |||
206 |
<h3>${_(' |
|
215 | <h3>${_('Locking')}</h3> | |
207 |
${h.form(url('repo', repo_name=c.repo_info.repo_name),method=' |
|
216 | ${h.form(url('repo_locking', repo_name=c.repo_info.repo_name),method='put')} | |
208 | <div class="form"> |
|
217 | <div class="form"> | |
209 | <div class="fields"> |
|
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 | </div> |
|
226 | </div> | |
212 | <div class="field" style="border:none;color:#888"> |
|
227 | <div class="field" style="border:none;color:#888"> | |
213 | <ul> |
|
228 | <ul> | |
214 | <li>${_('''This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems. |
|
229 | <li>${_('Force locking on repository. Works only when anonymous access is disabled')} | |
215 | If you need fully delete it from filesystem please do it manually''')} |
|
|||
216 | </li> |
|
230 | </li> | |
217 | </ul> |
|
231 | </ul> | |
218 | </div> |
|
232 | </div> | |
@@ -234,7 +248,21 b'' | |||||
234 | </div> |
|
248 | </div> | |
235 | ${h.end_form()} |
|
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 | </div> |
|
266 | </div> | |
238 |
|
267 | |||
239 |
|
||||
240 | </%def> |
|
268 | </%def> |
@@ -211,8 +211,8 b'' | |||||
211 | <label for="hooks_changegroup_push_logger">${_('Log user push commands')}</label> |
|
211 | <label for="hooks_changegroup_push_logger">${_('Log user push commands')}</label> | |
212 | </div> |
|
212 | </div> | |
213 | <div class="checkbox"> |
|
213 | <div class="checkbox"> | |
214 |
${h.checkbox('hooks_ |
|
214 | ${h.checkbox('hooks_outgoing_pull_logger','True')} | |
215 |
<label for="hooks_ |
|
215 | <label for="hooks_outgoing_pull_logger">${_('Log user pull commands')}</label> | |
216 | </div> |
|
216 | </div> | |
217 | </div> |
|
217 | </div> | |
218 | <div class="input" style="margin-top:10px"> |
|
218 | <div class="input" style="margin-top:10px"> |
General Comments 0
You need to be logged in to leave comments.
Login now