##// END OF EJS Templates
Implemented basic locking functionality....
marcink -
r2726:aa17c7a1 beta
parent child Browse files
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 mercurial protocol '
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_PULL
328 hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
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(username, action, repository, extras['ip'], commit=True)
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,20 +331,18 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 }
268 for k, v in extras.items():
269 baseui.setconfig('rhodecode_extras', k, v)
270 repo = repo.scm_instance
271 repo.ui = baseui
335 extras = json.loads(env['RHODECODE_EXTRAS'])
336 for k, v in extras.items():
337 baseui.setconfig('rhodecode_extras', k, v)
338 repo = repo.scm_instance
339 repo.ui = baseui
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):
272 346
273 347 rev_data = []
274 348 for l in revs:
@@ -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, username):
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.username = username
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['service']
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.base import _get_ip_addr
123 gitenv['RHODECODE_USER'] = self.username
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, username):
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.username = username
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.username)
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, username):
202 return GitDirectory(repo_root, repo_name, username)
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, username)
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, username):
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 username=username,
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 from datetime import datetime
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_REPO_SIZE,
252 cls.HOOK_PUSH, cls.HOOK_PULL]))
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_REPO_SIZE,
260 cls.HOOK_PUSH, cls.HOOK_PULL]))
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
@@ -744,7 +769,7 b' class Repository(Base, BaseModel):'
744 769 if ui_.ui_key == 'push_ssl':
745 770 # force set push_ssl requirement to False, rhodecode
746 771 # handles that
747 baseui.setconfig(ui_.ui_section, ui_.ui_key, False)
772 baseui.setconfig(ui_.ui_section, ui_.ui_key, False)
748 773
749 774 return baseui
750 775
@@ -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_preoutgoing_pull_logger = v.StringBoolean(if_missing=False)
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,34 +571,41 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')
579 _rhodecode_hook = False
580 log.debug('Installing git hook in repo %s' % repo)
581 if os.path.exists(_hook_file):
582 # let's take a look at this hook, maybe it's rhodecode ?
583 log.debug('hook exists, checking if it is from rhodecode')
584 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
585 with open(_hook_file, 'rb') as f:
586 data = f.read()
587 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
588 % 'RC_HOOK_VER').search(data)
589 if matches:
590 try:
591 ver = matches.groups()[0]
592 log.debug('got %s it is rhodecode' % (ver))
593 _rhodecode_hook = True
594 except:
595 log.error(traceback.format_exc())
581 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
582 _hook_file = jn(loc, '%s-receive' % h_type)
583 _rhodecode_hook = False
584 log.debug('Installing git hook in repo %s' % repo)
585 if os.path.exists(_hook_file):
586 # let's take a look at this hook, maybe it's rhodecode ?
587 log.debug('hook exists, checking if it is from rhodecode')
588 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
589 with open(_hook_file, 'rb') as f:
590 data = f.read()
591 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
592 % 'RC_HOOK_VER').search(data)
593 if matches:
594 try:
595 ver = matches.groups()[0]
596 log.debug('got %s it is rhodecode' % (ver))
597 _rhodecode_hook = True
598 except:
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:
598 log.debug('writing hook file !')
599 with open(_hook_file, 'wb') as f:
600 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
601 f.write(tmpl)
602 os.chmod(_hook_file, 0755)
603 else:
604 log.debug('skipping writing hook file')
604 if _rhodecode_hook or force_create:
605 log.debug('writing %s hook file !' % h_type)
606 with open(_hook_file, 'wb') as f:
607 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
608 f.write(tmpl)
609 os.chmod(_hook_file, 0755)
610 else:
611 log.debug('skipping writing hook file')
@@ -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,26 +205,31 b''
196 205 </div>
197 206 <div class="field" style="border:none;color:#888">
198 207 <ul>
199 <li>${_('''All actions made on this repository will be accessible to everyone in public journal''')}
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>${_('Delete')}</h3>
207 ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='delete')}
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 </div>
232 </div>
219 233 </div>
220 234 ${h.end_form()}
221 235
@@ -231,10 +245,24 b''
231 245 <li>${_('''Manually set this repository as a fork of another from the list''')}</li>
232 246 </ul>
233 247 </div>
234 </div>
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_preoutgoing_pull_logger','True')}
215 <label for="hooks_preoutgoing_pull_logger">${_('Log user pull commands')}</label>
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