##// END OF EJS Templates
Use space after , in lists
Mads Kiilerich -
r3987:b58ed6d6 default
parent child Browse files
Show More
@@ -1,348 +1,348 b''
1 """The base Controller API
1 """The base Controller API
2
2
3 Provides the BaseController class for subclassing.
3 Provides the BaseController class for subclassing.
4 """
4 """
5 import logging
5 import logging
6 import time
6 import time
7 import traceback
7 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 paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
11 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
12
12
13 from pylons import config, tmpl_context as c, request, session, url
13 from pylons import config, tmpl_context as c, request, session, url
14 from pylons.controllers import WSGIController
14 from pylons.controllers import WSGIController
15 from pylons.controllers.util import redirect
15 from pylons.controllers.util import redirect
16 from pylons.templating import render_mako as render
16 from pylons.templating import render_mako as render
17
17
18 from rhodecode import __version__, BACKENDS
18 from rhodecode import __version__, BACKENDS
19
19
20 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
20 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
21 safe_str, safe_int
21 safe_str, safe_int
22 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
22 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
23 HasPermissionAnyMiddleware, CookieStoreWrapper
23 HasPermissionAnyMiddleware, CookieStoreWrapper
24 from rhodecode.lib.utils import get_repo_slug
24 from rhodecode.lib.utils import get_repo_slug
25 from rhodecode.model import meta
25 from rhodecode.model import meta
26
26
27 from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting
27 from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting
28 from rhodecode.model.notification import NotificationModel
28 from rhodecode.model.notification import NotificationModel
29 from rhodecode.model.scm import ScmModel
29 from rhodecode.model.scm import ScmModel
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 def _filter_proxy(ip):
35 def _filter_proxy(ip):
36 """
36 """
37 HEADERS can have mutliple ips inside the left-most being the original
37 HEADERS can have multiple ips inside the left-most being the original
38 client, and each successive proxy that passed the request adding the IP
38 client, and each successive proxy that passed the request adding the IP
39 address where it received the request from.
39 address where it received the request from.
40
40
41 :param ip:
41 :param ip:
42 """
42 """
43 if ',' in ip:
43 if ',' in ip:
44 _ips = ip.split(',')
44 _ips = ip.split(',')
45 _first_ip = _ips[0].strip()
45 _first_ip = _ips[0].strip()
46 log.debug('Got multiple IPs %s, using %s' % (','.join(_ips), _first_ip))
46 log.debug('Got multiple IPs %s, using %s' % (','.join(_ips), _first_ip))
47 return _first_ip
47 return _first_ip
48 return ip
48 return ip
49
49
50
50
51 def _get_ip_addr(environ):
51 def _get_ip_addr(environ):
52 proxy_key = 'HTTP_X_REAL_IP'
52 proxy_key = 'HTTP_X_REAL_IP'
53 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
53 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
54 def_key = 'REMOTE_ADDR'
54 def_key = 'REMOTE_ADDR'
55
55
56 ip = environ.get(proxy_key)
56 ip = environ.get(proxy_key)
57 if ip:
57 if ip:
58 return _filter_proxy(ip)
58 return _filter_proxy(ip)
59
59
60 ip = environ.get(proxy_key2)
60 ip = environ.get(proxy_key2)
61 if ip:
61 if ip:
62 return _filter_proxy(ip)
62 return _filter_proxy(ip)
63
63
64 ip = environ.get(def_key, '0.0.0.0')
64 ip = environ.get(def_key, '0.0.0.0')
65 return _filter_proxy(ip)
65 return _filter_proxy(ip)
66
66
67
67
68 def _get_access_path(environ):
68 def _get_access_path(environ):
69 path = environ.get('PATH_INFO')
69 path = environ.get('PATH_INFO')
70 org_req = environ.get('pylons.original_request')
70 org_req = environ.get('pylons.original_request')
71 if org_req:
71 if org_req:
72 path = org_req.environ.get('PATH_INFO')
72 path = org_req.environ.get('PATH_INFO')
73 return path
73 return path
74
74
75
75
76 class BasicAuth(AuthBasicAuthenticator):
76 class BasicAuth(AuthBasicAuthenticator):
77
77
78 def __init__(self, realm, authfunc, auth_http_code=None):
78 def __init__(self, realm, authfunc, auth_http_code=None):
79 self.realm = realm
79 self.realm = realm
80 self.authfunc = authfunc
80 self.authfunc = authfunc
81 self._rc_auth_http_code = auth_http_code
81 self._rc_auth_http_code = auth_http_code
82
82
83 def build_authentication(self):
83 def build_authentication(self):
84 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
84 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
85 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
85 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
86 # return 403 if alternative http return code is specified in
86 # return 403 if alternative http return code is specified in
87 # RhodeCode config
87 # RhodeCode config
88 return HTTPForbidden(headers=head)
88 return HTTPForbidden(headers=head)
89 return HTTPUnauthorized(headers=head)
89 return HTTPUnauthorized(headers=head)
90
90
91 def authenticate(self, environ):
91 def authenticate(self, environ):
92 authorization = AUTHORIZATION(environ)
92 authorization = AUTHORIZATION(environ)
93 if not authorization:
93 if not authorization:
94 return self.build_authentication()
94 return self.build_authentication()
95 (authmeth, auth) = authorization.split(' ', 1)
95 (authmeth, auth) = authorization.split(' ', 1)
96 if 'basic' != authmeth.lower():
96 if 'basic' != authmeth.lower():
97 return self.build_authentication()
97 return self.build_authentication()
98 auth = auth.strip().decode('base64')
98 auth = auth.strip().decode('base64')
99 _parts = auth.split(':', 1)
99 _parts = auth.split(':', 1)
100 if len(_parts) == 2:
100 if len(_parts) == 2:
101 username, password = _parts
101 username, password = _parts
102 if self.authfunc(environ, username, password):
102 if self.authfunc(environ, username, password):
103 return username
103 return username
104 return self.build_authentication()
104 return self.build_authentication()
105
105
106 __call__ = authenticate
106 __call__ = authenticate
107
107
108
108
109 class BaseVCSController(object):
109 class BaseVCSController(object):
110
110
111 def __init__(self, application, config):
111 def __init__(self, application, config):
112 self.application = application
112 self.application = application
113 self.config = config
113 self.config = config
114 # base path of repo locations
114 # base path of repo locations
115 self.basepath = self.config['base_path']
115 self.basepath = self.config['base_path']
116 #authenticate this mercurial request using authfunc
116 #authenticate this mercurial request using authfunc
117 self.authenticate = BasicAuth('', authfunc,
117 self.authenticate = BasicAuth('', authfunc,
118 config.get('auth_ret_code'))
118 config.get('auth_ret_code'))
119 self.ip_addr = '0.0.0.0'
119 self.ip_addr = '0.0.0.0'
120
120
121 def _handle_request(self, environ, start_response):
121 def _handle_request(self, environ, start_response):
122 raise NotImplementedError()
122 raise NotImplementedError()
123
123
124 def _get_by_id(self, repo_name):
124 def _get_by_id(self, repo_name):
125 """
125 """
126 Get's a special pattern _<ID> from clone url and tries to replace it
126 Get's a special pattern _<ID> from clone url and tries to replace it
127 with a repository_name for support of _<ID> non changable urls
127 with a repository_name for support of _<ID> non changable urls
128
128
129 :param repo_name:
129 :param repo_name:
130 """
130 """
131 try:
131 try:
132 data = repo_name.split('/')
132 data = repo_name.split('/')
133 if len(data) >= 2:
133 if len(data) >= 2:
134 by_id = data[1].split('_')
134 by_id = data[1].split('_')
135 if len(by_id) == 2 and by_id[1].isdigit():
135 if len(by_id) == 2 and by_id[1].isdigit():
136 _repo_name = Repository.get(by_id[1]).repo_name
136 _repo_name = Repository.get(by_id[1]).repo_name
137 data[1] = _repo_name
137 data[1] = _repo_name
138 except Exception:
138 except Exception:
139 log.debug('Failed to extract repo_name from id %s' % (
139 log.debug('Failed to extract repo_name from id %s' % (
140 traceback.format_exc()
140 traceback.format_exc()
141 )
141 )
142 )
142 )
143
143
144 return '/'.join(data)
144 return '/'.join(data)
145
145
146 def _invalidate_cache(self, repo_name):
146 def _invalidate_cache(self, repo_name):
147 """
147 """
148 Set's cache for this repository for invalidation on next access
148 Set's cache for this repository for invalidation on next access
149
149
150 :param repo_name: full repo name, also a cache key
150 :param repo_name: full repo name, also a cache key
151 """
151 """
152 ScmModel().mark_for_invalidation(repo_name)
152 ScmModel().mark_for_invalidation(repo_name)
153
153
154 def _check_permission(self, action, user, repo_name, ip_addr=None):
154 def _check_permission(self, action, user, repo_name, ip_addr=None):
155 """
155 """
156 Checks permissions using action (push/pull) user and repository
156 Checks permissions using action (push/pull) user and repository
157 name
157 name
158
158
159 :param action: push or pull action
159 :param action: push or pull action
160 :param user: user instance
160 :param user: user instance
161 :param repo_name: repository name
161 :param repo_name: repository name
162 """
162 """
163 #check IP
163 #check IP
164 authuser = AuthUser(user_id=user.user_id, ip_addr=ip_addr)
164 authuser = AuthUser(user_id=user.user_id, ip_addr=ip_addr)
165 if not authuser.ip_allowed:
165 if not authuser.ip_allowed:
166 return False
166 return False
167 else:
167 else:
168 log.info('Access for IP:%s allowed' % (ip_addr))
168 log.info('Access for IP:%s allowed' % (ip_addr))
169 if action == 'push':
169 if action == 'push':
170 if not HasPermissionAnyMiddleware('repository.write',
170 if not HasPermissionAnyMiddleware('repository.write',
171 'repository.admin')(user,
171 'repository.admin')(user,
172 repo_name):
172 repo_name):
173 return False
173 return False
174
174
175 else:
175 else:
176 #any other action need at least read permission
176 #any other action need at least read permission
177 if not HasPermissionAnyMiddleware('repository.read',
177 if not HasPermissionAnyMiddleware('repository.read',
178 'repository.write',
178 'repository.write',
179 'repository.admin')(user,
179 'repository.admin')(user,
180 repo_name):
180 repo_name):
181 return False
181 return False
182
182
183 return True
183 return True
184
184
185 def _get_ip_addr(self, environ):
185 def _get_ip_addr(self, environ):
186 return _get_ip_addr(environ)
186 return _get_ip_addr(environ)
187
187
188 def _check_ssl(self, environ, start_response):
188 def _check_ssl(self, environ, start_response):
189 """
189 """
190 Checks the SSL check flag and returns False if SSL is not present
190 Checks the SSL check flag and returns False if SSL is not present
191 and required True otherwise
191 and required True otherwise
192 """
192 """
193 org_proto = environ['wsgi._org_proto']
193 org_proto = environ['wsgi._org_proto']
194 #check if we have SSL required ! if not it's a bad request !
194 #check if we have SSL required ! if not it's a bad request !
195 require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl').ui_value)
195 require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl').ui_value)
196 if require_ssl and org_proto == 'http':
196 if require_ssl and org_proto == 'http':
197 log.debug('proto is %s and SSL is required BAD REQUEST !'
197 log.debug('proto is %s and SSL is required BAD REQUEST !'
198 % org_proto)
198 % org_proto)
199 return False
199 return False
200 return True
200 return True
201
201
202 def _check_locking_state(self, environ, action, repo, user_id):
202 def _check_locking_state(self, environ, action, repo, user_id):
203 """
203 """
204 Checks locking on this repository, if locking is enabled and lock is
204 Checks locking on this repository, if locking is enabled and lock is
205 present returns a tuple of make_lock, locked, locked_by.
205 present returns a tuple of make_lock, locked, locked_by.
206 make_lock can have 3 states None (do nothing) True, make lock
206 make_lock can have 3 states None (do nothing) True, make lock
207 False release lock, This value is later propagated to hooks, which
207 False release lock, This value is later propagated to hooks, which
208 do the locking. Think about this as signals passed to hooks what to do.
208 do the locking. Think about this as signals passed to hooks what to do.
209
209
210 """
210 """
211 locked = False # defines that locked error should be thrown to user
211 locked = False # defines that locked error should be thrown to user
212 make_lock = None
212 make_lock = None
213 repo = Repository.get_by_repo_name(repo)
213 repo = Repository.get_by_repo_name(repo)
214 user = User.get(user_id)
214 user = User.get(user_id)
215
215
216 # this is kind of hacky, but due to how mercurial handles client-server
216 # this is kind of hacky, but due to how mercurial handles client-server
217 # server see all operation on changeset; bookmarks, phases and
217 # server see all operation on changeset; bookmarks, phases and
218 # obsolescence marker in different transaction, we don't want to check
218 # obsolescence marker in different transaction, we don't want to check
219 # locking on those
219 # locking on those
220 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
220 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
221 locked_by = repo.locked
221 locked_by = repo.locked
222 if repo and repo.enable_locking and not obsolete_call:
222 if repo and repo.enable_locking and not obsolete_call:
223 if action == 'push':
223 if action == 'push':
224 #check if it's already locked !, if it is compare users
224 #check if it's already locked !, if it is compare users
225 user_id, _date = repo.locked
225 user_id, _date = repo.locked
226 if user.user_id == user_id:
226 if user.user_id == user_id:
227 log.debug('Got push from user %s, now unlocking' % (user))
227 log.debug('Got push from user %s, now unlocking' % (user))
228 # unlock if we have push from user who locked
228 # unlock if we have push from user who locked
229 make_lock = False
229 make_lock = False
230 else:
230 else:
231 # we're not the same user who locked, ban with 423 !
231 # we're not the same user who locked, ban with 423 !
232 locked = True
232 locked = True
233 if action == 'pull':
233 if action == 'pull':
234 if repo.locked[0] and repo.locked[1]:
234 if repo.locked[0] and repo.locked[1]:
235 locked = True
235 locked = True
236 else:
236 else:
237 log.debug('Setting lock on repo %s by %s' % (repo, user))
237 log.debug('Setting lock on repo %s by %s' % (repo, user))
238 make_lock = True
238 make_lock = True
239
239
240 else:
240 else:
241 log.debug('Repository %s do not have locking enabled' % (repo))
241 log.debug('Repository %s do not have locking enabled' % (repo))
242 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
242 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
243 % (make_lock, locked, locked_by))
243 % (make_lock, locked, locked_by))
244 return make_lock, locked, locked_by
244 return make_lock, locked, locked_by
245
245
246 def __call__(self, environ, start_response):
246 def __call__(self, environ, start_response):
247 start = time.time()
247 start = time.time()
248 try:
248 try:
249 return self._handle_request(environ, start_response)
249 return self._handle_request(environ, start_response)
250 finally:
250 finally:
251 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
251 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
252 log.debug('Request time: %.3fs' % (time.time() - start))
252 log.debug('Request time: %.3fs' % (time.time() - start))
253 meta.Session.remove()
253 meta.Session.remove()
254
254
255
255
256 class BaseController(WSGIController):
256 class BaseController(WSGIController):
257
257
258 def __before__(self):
258 def __before__(self):
259 """
259 """
260 __before__ is called before controller methods and after __call__
260 __before__ is called before controller methods and after __call__
261 """
261 """
262 c.rhodecode_version = __version__
262 c.rhodecode_version = __version__
263 c.rhodecode_instanceid = config.get('instance_id')
263 c.rhodecode_instanceid = config.get('instance_id')
264 c.rhodecode_name = config.get('rhodecode_title')
264 c.rhodecode_name = config.get('rhodecode_title')
265 c.use_gravatar = str2bool(config.get('use_gravatar'))
265 c.use_gravatar = str2bool(config.get('use_gravatar'))
266 c.ga_code = config.get('rhodecode_ga_code')
266 c.ga_code = config.get('rhodecode_ga_code')
267 # Visual options
267 # Visual options
268 c.visual = AttributeDict({})
268 c.visual = AttributeDict({})
269 rc_config = RhodeCodeSetting.get_app_settings()
269 rc_config = RhodeCodeSetting.get_app_settings()
270 ## DB stored
270 ## DB stored
271 c.visual.show_public_icon = str2bool(rc_config.get('rhodecode_show_public_icon'))
271 c.visual.show_public_icon = str2bool(rc_config.get('rhodecode_show_public_icon'))
272 c.visual.show_private_icon = str2bool(rc_config.get('rhodecode_show_private_icon'))
272 c.visual.show_private_icon = str2bool(rc_config.get('rhodecode_show_private_icon'))
273 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
273 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
274 c.visual.dashboard_items = safe_int(rc_config.get('rhodecode_dashboard_items', 100))
274 c.visual.dashboard_items = safe_int(rc_config.get('rhodecode_dashboard_items', 100))
275 c.visual.repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
275 c.visual.repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
276 c.visual.show_version = str2bool(rc_config.get('rhodecode_show_version'))
276 c.visual.show_version = str2bool(rc_config.get('rhodecode_show_version'))
277
277
278 ## INI stored
278 ## INI stored
279 self.cut_off_limit = int(config.get('cut_off_limit'))
279 self.cut_off_limit = int(config.get('cut_off_limit'))
280 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
280 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
281
281
282 c.repo_name = get_repo_slug(request) # can be empty
282 c.repo_name = get_repo_slug(request) # can be empty
283 c.backends = BACKENDS.keys()
283 c.backends = BACKENDS.keys()
284 c.unread_notifications = NotificationModel()\
284 c.unread_notifications = NotificationModel()\
285 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
285 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
286 self.sa = meta.Session
286 self.sa = meta.Session
287 self.scm_model = ScmModel(self.sa)
287 self.scm_model = ScmModel(self.sa)
288
288
289 def __call__(self, environ, start_response):
289 def __call__(self, environ, start_response):
290 """Invoke the Controller"""
290 """Invoke the Controller"""
291 # WSGIController.__call__ dispatches to the Controller method
291 # WSGIController.__call__ dispatches to the Controller method
292 # the request is routed to. This routing information is
292 # the request is routed to. This routing information is
293 # available in environ['pylons.routes_dict']
293 # available in environ['pylons.routes_dict']
294 try:
294 try:
295 self.ip_addr = _get_ip_addr(environ)
295 self.ip_addr = _get_ip_addr(environ)
296 # make sure that we update permissions each time we call controller
296 # make sure that we update permissions each time we call controller
297 api_key = request.GET.get('api_key')
297 api_key = request.GET.get('api_key')
298 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
298 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
299 user_id = cookie_store.get('user_id', None)
299 user_id = cookie_store.get('user_id', None)
300 username = get_container_username(environ, config)
300 username = get_container_username(environ, config)
301 auth_user = AuthUser(user_id, api_key, username, self.ip_addr)
301 auth_user = AuthUser(user_id, api_key, username, self.ip_addr)
302 request.user = auth_user
302 request.user = auth_user
303 self.rhodecode_user = c.rhodecode_user = auth_user
303 self.rhodecode_user = c.rhodecode_user = auth_user
304 if not self.rhodecode_user.is_authenticated and \
304 if not self.rhodecode_user.is_authenticated and \
305 self.rhodecode_user.user_id is not None:
305 self.rhodecode_user.user_id is not None:
306 self.rhodecode_user.set_authenticated(
306 self.rhodecode_user.set_authenticated(
307 cookie_store.get('is_authenticated')
307 cookie_store.get('is_authenticated')
308 )
308 )
309 log.info('IP: %s User: %s accessed %s' % (
309 log.info('IP: %s User: %s accessed %s' % (
310 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
310 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
311 )
311 )
312 return WSGIController.__call__(self, environ, start_response)
312 return WSGIController.__call__(self, environ, start_response)
313 finally:
313 finally:
314 meta.Session.remove()
314 meta.Session.remove()
315
315
316
316
317 class BaseRepoController(BaseController):
317 class BaseRepoController(BaseController):
318 """
318 """
319 Base class for controllers responsible for loading all needed data for
319 Base class for controllers responsible for loading all needed data for
320 repository loaded items are
320 repository loaded items are
321
321
322 c.rhodecode_repo: instance of scm repository
322 c.rhodecode_repo: instance of scm repository
323 c.rhodecode_db_repo: instance of db
323 c.rhodecode_db_repo: instance of db
324 c.repository_followers: number of followers
324 c.repository_followers: number of followers
325 c.repository_forks: number of forks
325 c.repository_forks: number of forks
326 c.repository_following: weather the current user is following the current repo
326 c.repository_following: weather the current user is following the current repo
327 """
327 """
328
328
329 def __before__(self):
329 def __before__(self):
330 super(BaseRepoController, self).__before__()
330 super(BaseRepoController, self).__before__()
331 if c.repo_name:
331 if c.repo_name:
332
332
333 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
333 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
334 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
334 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
335 # update last change according to VCS data
335 # update last change according to VCS data
336 dbr.update_changeset_cache(dbr.get_changeset())
336 dbr.update_changeset_cache(dbr.get_changeset())
337 if c.rhodecode_repo is None:
337 if c.rhodecode_repo is None:
338 log.error('%s this repository is present in database but it '
338 log.error('%s this repository is present in database but it '
339 'cannot be created as an scm instance', c.repo_name)
339 'cannot be created as an scm instance', c.repo_name)
340
340
341 redirect(url('home'))
341 redirect(url('home'))
342
342
343 # some globals counter for menu
343 # some globals counter for menu
344 c.repository_followers = self.scm_model.get_followers(dbr)
344 c.repository_followers = self.scm_model.get_followers(dbr)
345 c.repository_forks = self.scm_model.get_forks(dbr)
345 c.repository_forks = self.scm_model.get_forks(dbr)
346 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
346 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
347 c.repository_following = self.scm_model.is_following_repo(c.repo_name,
347 c.repository_following = self.scm_model.is_following_repo(c.repo_name,
348 self.rhodecode_user.user_id)
348 self.rhodecode_user.user_id)
@@ -1,70 +1,70 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.paster_commands.make_rcextensions
3 rhodecode.lib.paster_commands.make_rcextensions
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 repo-scan paster command for RhodeCode
6 repo-scan paster command for RhodeCode
7
7
8
8
9 :created_on: Feb 9, 2013
9 :created_on: Feb 9, 2013
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2013 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2013 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 from __future__ import with_statement
26 from __future__ import with_statement
27
27
28 import os
28 import os
29 import sys
29 import sys
30 import logging
30 import logging
31
31
32 from rhodecode.model.scm import ScmModel
32 from rhodecode.model.scm import ScmModel
33 from rhodecode.lib.utils import BasePasterCommand, repo2db_mapper
33 from rhodecode.lib.utils import BasePasterCommand, repo2db_mapper
34
34
35 # fix rhodecode import
35 # fix rhodecode import
36 from os.path import dirname as dn
36 from os.path import dirname as dn
37 rc_path = dn(dn(dn(os.path.realpath(__file__))))
37 rc_path = dn(dn(dn(os.path.realpath(__file__))))
38 sys.path.append(rc_path)
38 sys.path.append(rc_path)
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class Command(BasePasterCommand):
43 class Command(BasePasterCommand):
44
44
45 max_args = 1
45 max_args = 1
46 min_args = 1
46 min_args = 1
47
47
48 usage = "CONFIG_FILE"
48 usage = "CONFIG_FILE"
49 group_name = "RhodeCode"
49 group_name = "RhodeCode"
50 takes_config_file = -1
50 takes_config_file = -1
51 parser = BasePasterCommand.standard_parser(verbose=True)
51 parser = BasePasterCommand.standard_parser(verbose=True)
52 summary = "Rescan default location for new repositories"
52 summary = "Rescan default location for new repositories"
53
53
54 def command(self):
54 def command(self):
55 #get SqlAlchemy session
55 #get SqlAlchemy session
56 self._init_session()
56 self._init_session()
57 rm_obsolete = self.options.delete_obsolete
57 rm_obsolete = self.options.delete_obsolete
58 log.info('Now scanning root location for new repos...')
58 log.info('Now scanning root location for new repos...')
59 added, removed = repo2db_mapper(ScmModel().repo_scan(),
59 added, removed = repo2db_mapper(ScmModel().repo_scan(),
60 remove_obsolete=rm_obsolete)
60 remove_obsolete=rm_obsolete)
61 added = ','.join(added) or '-'
61 added = ', '.join(added) or '-'
62 removed = ','.join(removed) or '-'
62 removed = ', '.join(removed) or '-'
63 log.info('Scan completed added:%s removed:%s' % (added, removed))
63 log.info('Scan completed added: %s removed:%s' % (added, removed))
64
64
65 def update_parser(self):
65 def update_parser(self):
66 self.parser.add_option('--delete-obsolete',
66 self.parser.add_option('--delete-obsolete',
67 action='store_true',
67 action='store_true',
68 help="Use this flag do delete repositories that are "
68 help="Use this flag do delete repositories that are "
69 "present in RhodeCode database but not on the filesystem",
69 "present in RhodeCode database but not on the filesystem",
70 )
70 )
@@ -1,243 +1,243 b''
1 """
1 """
2 Utitlites aimed to help achieve mostly basic tasks.
2 Utitlites aimed to help achieve mostly basic tasks.
3 """
3 """
4 from __future__ import division
4 from __future__ import division
5
5
6 import re
6 import re
7 import os
7 import os
8 import time
8 import time
9 import datetime
9 import datetime
10 from subprocess import Popen, PIPE
10 from subprocess import Popen, PIPE
11
11
12 from rhodecode.lib.vcs.exceptions import VCSError
12 from rhodecode.lib.vcs.exceptions import VCSError
13 from rhodecode.lib.vcs.exceptions import RepositoryError
13 from rhodecode.lib.vcs.exceptions import RepositoryError
14 from rhodecode.lib.vcs.utils.paths import abspath
14 from rhodecode.lib.vcs.utils.paths import abspath
15
15
16 ALIASES = ['hg', 'git']
16 ALIASES = ['hg', 'git']
17
17
18
18
19 def get_scm(path, search_up=False, explicit_alias=None):
19 def get_scm(path, search_up=False, explicit_alias=None):
20 """
20 """
21 Returns one of alias from ``ALIASES`` (in order of precedence same as
21 Returns one of alias from ``ALIASES`` (in order of precedence same as
22 shortcuts given in ``ALIASES``) and top working dir path for the given
22 shortcuts given in ``ALIASES``) and top working dir path for the given
23 argument. If no scm-specific directory is found or more than one scm is
23 argument. If no scm-specific directory is found or more than one scm is
24 found at that directory, ``VCSError`` is raised.
24 found at that directory, ``VCSError`` is raised.
25
25
26 :param search_up: if set to ``True``, this function would try to
26 :param search_up: if set to ``True``, this function would try to
27 move up to parent directory every time no scm is recognized for the
27 move up to parent directory every time no scm is recognized for the
28 currently checked path. Default: ``False``.
28 currently checked path. Default: ``False``.
29 :param explicit_alias: can be one of available backend aliases, when given
29 :param explicit_alias: can be one of available backend aliases, when given
30 it will return given explicit alias in repositories under more than one
30 it will return given explicit alias in repositories under more than one
31 version control, if explicit_alias is different than found it will raise
31 version control, if explicit_alias is different than found it will raise
32 VCSError
32 VCSError
33 """
33 """
34 if not os.path.isdir(path):
34 if not os.path.isdir(path):
35 raise VCSError("Given path %s is not a directory" % path)
35 raise VCSError("Given path %s is not a directory" % path)
36
36
37 def get_scms(path):
37 def get_scms(path):
38 return [(scm, path) for scm in get_scms_for_path(path)]
38 return [(scm, path) for scm in get_scms_for_path(path)]
39
39
40 found_scms = get_scms(path)
40 found_scms = get_scms(path)
41 while not found_scms and search_up:
41 while not found_scms and search_up:
42 newpath = abspath(path, '..')
42 newpath = abspath(path, '..')
43 if newpath == path:
43 if newpath == path:
44 break
44 break
45 path = newpath
45 path = newpath
46 found_scms = get_scms(path)
46 found_scms = get_scms(path)
47
47
48 if len(found_scms) > 1:
48 if len(found_scms) > 1:
49 for scm in found_scms:
49 for scm in found_scms:
50 if scm[0] == explicit_alias:
50 if scm[0] == explicit_alias:
51 return scm
51 return scm
52 raise VCSError('More than one [%s] scm found at given path %s'
52 raise VCSError('More than one [%s] scm found at given path %s'
53 % (','.join((x[0] for x in found_scms)), path))
53 % (', '.join((x[0] for x in found_scms)), path))
54
54
55 if len(found_scms) is 0:
55 if len(found_scms) is 0:
56 raise VCSError('No scm found at given path %s' % path)
56 raise VCSError('No scm found at given path %s' % path)
57
57
58 return found_scms[0]
58 return found_scms[0]
59
59
60
60
61 def get_scms_for_path(path):
61 def get_scms_for_path(path):
62 """
62 """
63 Returns all scm's found at the given path. If no scm is recognized
63 Returns all scm's found at the given path. If no scm is recognized
64 - empty list is returned.
64 - empty list is returned.
65
65
66 :param path: path to directory which should be checked. May be callable.
66 :param path: path to directory which should be checked. May be callable.
67
67
68 :raises VCSError: if given ``path`` is not a directory
68 :raises VCSError: if given ``path`` is not a directory
69 """
69 """
70 from rhodecode.lib.vcs.backends import get_backend
70 from rhodecode.lib.vcs.backends import get_backend
71 if hasattr(path, '__call__'):
71 if hasattr(path, '__call__'):
72 path = path()
72 path = path()
73 if not os.path.isdir(path):
73 if not os.path.isdir(path):
74 raise VCSError("Given path %r is not a directory" % path)
74 raise VCSError("Given path %r is not a directory" % path)
75
75
76 result = []
76 result = []
77 for key in ALIASES:
77 for key in ALIASES:
78 dirname = os.path.join(path, '.' + key)
78 dirname = os.path.join(path, '.' + key)
79 if os.path.isdir(dirname):
79 if os.path.isdir(dirname):
80 result.append(key)
80 result.append(key)
81 continue
81 continue
82 dirname = os.path.join(path, 'rm__.' + key)
82 dirname = os.path.join(path, 'rm__.' + key)
83 if os.path.isdir(dirname):
83 if os.path.isdir(dirname):
84 return result
84 return result
85 # We still need to check if it's not bare repository as
85 # We still need to check if it's not bare repository as
86 # bare repos don't have working directories
86 # bare repos don't have working directories
87 try:
87 try:
88 get_backend(key)(path)
88 get_backend(key)(path)
89 result.append(key)
89 result.append(key)
90 continue
90 continue
91 except RepositoryError:
91 except RepositoryError:
92 # Wrong backend
92 # Wrong backend
93 pass
93 pass
94 except VCSError:
94 except VCSError:
95 # No backend at all
95 # No backend at all
96 pass
96 pass
97 return result
97 return result
98
98
99
99
100 def run_command(cmd, *args):
100 def run_command(cmd, *args):
101 """
101 """
102 Runs command on the system with given ``args``.
102 Runs command on the system with given ``args``.
103 """
103 """
104 command = ' '.join((cmd, args))
104 command = ' '.join((cmd, args))
105 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
105 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
106 stdout, stderr = p.communicate()
106 stdout, stderr = p.communicate()
107 return p.retcode, stdout, stderr
107 return p.retcode, stdout, stderr
108
108
109
109
110 def get_highlighted_code(name, code, type='terminal'):
110 def get_highlighted_code(name, code, type='terminal'):
111 """
111 """
112 If pygments are available on the system
112 If pygments are available on the system
113 then returned output is colored. Otherwise
113 then returned output is colored. Otherwise
114 unchanged content is returned.
114 unchanged content is returned.
115 """
115 """
116 import logging
116 import logging
117 try:
117 try:
118 import pygments
118 import pygments
119 pygments
119 pygments
120 except ImportError:
120 except ImportError:
121 return code
121 return code
122 from pygments import highlight
122 from pygments import highlight
123 from pygments.lexers import guess_lexer_for_filename, ClassNotFound
123 from pygments.lexers import guess_lexer_for_filename, ClassNotFound
124 from pygments.formatters import TerminalFormatter
124 from pygments.formatters import TerminalFormatter
125
125
126 try:
126 try:
127 lexer = guess_lexer_for_filename(name, code)
127 lexer = guess_lexer_for_filename(name, code)
128 formatter = TerminalFormatter()
128 formatter = TerminalFormatter()
129 content = highlight(code, lexer, formatter)
129 content = highlight(code, lexer, formatter)
130 except ClassNotFound:
130 except ClassNotFound:
131 logging.debug("Couldn't guess Lexer, will not use pygments.")
131 logging.debug("Couldn't guess Lexer, will not use pygments.")
132 content = code
132 content = code
133 return content
133 return content
134
134
135
135
136 def parse_changesets(text):
136 def parse_changesets(text):
137 """
137 """
138 Returns dictionary with *start*, *main* and *end* ids.
138 Returns dictionary with *start*, *main* and *end* ids.
139
139
140 Examples::
140 Examples::
141
141
142 >>> parse_changesets('aaabbb')
142 >>> parse_changesets('aaabbb')
143 {'start': None, 'main': 'aaabbb', 'end': None}
143 {'start': None, 'main': 'aaabbb', 'end': None}
144 >>> parse_changesets('aaabbb..cccddd')
144 >>> parse_changesets('aaabbb..cccddd')
145 {'start': 'aaabbb', 'main': None, 'end': 'cccddd'}
145 {'start': 'aaabbb', 'main': None, 'end': 'cccddd'}
146
146
147 """
147 """
148 text = text.strip()
148 text = text.strip()
149 CID_RE = r'[a-zA-Z0-9]+'
149 CID_RE = r'[a-zA-Z0-9]+'
150 if not '..' in text:
150 if not '..' in text:
151 m = re.match(r'^(?P<cid>%s)$' % CID_RE, text)
151 m = re.match(r'^(?P<cid>%s)$' % CID_RE, text)
152 if m:
152 if m:
153 return {
153 return {
154 'start': None,
154 'start': None,
155 'main': text,
155 'main': text,
156 'end': None,
156 'end': None,
157 }
157 }
158 else:
158 else:
159 RE = r'^(?P<start>%s)?\.{2,3}(?P<end>%s)?$' % (CID_RE, CID_RE)
159 RE = r'^(?P<start>%s)?\.{2,3}(?P<end>%s)?$' % (CID_RE, CID_RE)
160 m = re.match(RE, text)
160 m = re.match(RE, text)
161 if m:
161 if m:
162 result = m.groupdict()
162 result = m.groupdict()
163 result['main'] = None
163 result['main'] = None
164 return result
164 return result
165 raise ValueError("IDs not recognized")
165 raise ValueError("IDs not recognized")
166
166
167
167
168 def parse_datetime(text):
168 def parse_datetime(text):
169 """
169 """
170 Parses given text and returns ``datetime.datetime`` instance or raises
170 Parses given text and returns ``datetime.datetime`` instance or raises
171 ``ValueError``.
171 ``ValueError``.
172
172
173 :param text: string of desired date/datetime or something more verbose,
173 :param text: string of desired date/datetime or something more verbose,
174 like *yesterday*, *2weeks 3days*, etc.
174 like *yesterday*, *2weeks 3days*, etc.
175 """
175 """
176
176
177 text = text.strip().lower()
177 text = text.strip().lower()
178
178
179 INPUT_FORMATS = (
179 INPUT_FORMATS = (
180 '%Y-%m-%d %H:%M:%S',
180 '%Y-%m-%d %H:%M:%S',
181 '%Y-%m-%d %H:%M',
181 '%Y-%m-%d %H:%M',
182 '%Y-%m-%d',
182 '%Y-%m-%d',
183 '%m/%d/%Y %H:%M:%S',
183 '%m/%d/%Y %H:%M:%S',
184 '%m/%d/%Y %H:%M',
184 '%m/%d/%Y %H:%M',
185 '%m/%d/%Y',
185 '%m/%d/%Y',
186 '%m/%d/%y %H:%M:%S',
186 '%m/%d/%y %H:%M:%S',
187 '%m/%d/%y %H:%M',
187 '%m/%d/%y %H:%M',
188 '%m/%d/%y',
188 '%m/%d/%y',
189 )
189 )
190 for format in INPUT_FORMATS:
190 for format in INPUT_FORMATS:
191 try:
191 try:
192 return datetime.datetime(*time.strptime(text, format)[:6])
192 return datetime.datetime(*time.strptime(text, format)[:6])
193 except ValueError:
193 except ValueError:
194 pass
194 pass
195
195
196 # Try descriptive texts
196 # Try descriptive texts
197 if text == 'tomorrow':
197 if text == 'tomorrow':
198 future = datetime.datetime.now() + datetime.timedelta(days=1)
198 future = datetime.datetime.now() + datetime.timedelta(days=1)
199 args = future.timetuple()[:3] + (23, 59, 59)
199 args = future.timetuple()[:3] + (23, 59, 59)
200 return datetime.datetime(*args)
200 return datetime.datetime(*args)
201 elif text == 'today':
201 elif text == 'today':
202 return datetime.datetime(*datetime.datetime.today().timetuple()[:3])
202 return datetime.datetime(*datetime.datetime.today().timetuple()[:3])
203 elif text == 'now':
203 elif text == 'now':
204 return datetime.datetime.now()
204 return datetime.datetime.now()
205 elif text == 'yesterday':
205 elif text == 'yesterday':
206 past = datetime.datetime.now() - datetime.timedelta(days=1)
206 past = datetime.datetime.now() - datetime.timedelta(days=1)
207 return datetime.datetime(*past.timetuple()[:3])
207 return datetime.datetime(*past.timetuple()[:3])
208 else:
208 else:
209 days = 0
209 days = 0
210 matched = re.match(
210 matched = re.match(
211 r'^((?P<weeks>\d+) ?w(eeks?)?)? ?((?P<days>\d+) ?d(ays?)?)?$', text)
211 r'^((?P<weeks>\d+) ?w(eeks?)?)? ?((?P<days>\d+) ?d(ays?)?)?$', text)
212 if matched:
212 if matched:
213 groupdict = matched.groupdict()
213 groupdict = matched.groupdict()
214 if groupdict['days']:
214 if groupdict['days']:
215 days += int(matched.groupdict()['days'])
215 days += int(matched.groupdict()['days'])
216 if groupdict['weeks']:
216 if groupdict['weeks']:
217 days += int(matched.groupdict()['weeks']) * 7
217 days += int(matched.groupdict()['weeks']) * 7
218 past = datetime.datetime.now() - datetime.timedelta(days=days)
218 past = datetime.datetime.now() - datetime.timedelta(days=days)
219 return datetime.datetime(*past.timetuple()[:3])
219 return datetime.datetime(*past.timetuple()[:3])
220
220
221 raise ValueError('Wrong date: "%s"' % text)
221 raise ValueError('Wrong date: "%s"' % text)
222
222
223
223
224 def get_dict_for_attrs(obj, attrs):
224 def get_dict_for_attrs(obj, attrs):
225 """
225 """
226 Returns dictionary for each attribute from given ``obj``.
226 Returns dictionary for each attribute from given ``obj``.
227 """
227 """
228 data = {}
228 data = {}
229 for attr in attrs:
229 for attr in attrs:
230 data[attr] = getattr(obj, attr)
230 data[attr] = getattr(obj, attr)
231 return data
231 return data
232
232
233
233
234 def get_total_seconds(timedelta):
234 def get_total_seconds(timedelta):
235 """
235 """
236 Backported for Python 2.5.
236 Backported for Python 2.5.
237
237
238 See http://docs.python.org/library/datetime.html.
238 See http://docs.python.org/library/datetime.html.
239 """
239 """
240 return ((timedelta.microseconds + (
240 return ((timedelta.microseconds + (
241 timedelta.seconds +
241 timedelta.seconds +
242 timedelta.days * 24 * 60 * 60
242 timedelta.days * 24 * 60 * 60
243 ) * 10**6) / 10**6)
243 ) * 10**6) / 10**6)
@@ -1,252 +1,252 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)} &middot; ${c.rhodecode_name}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)} &middot; ${c.rhodecode_name}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${_('Pull request #%s') % c.pull_request.pull_request_id}
8 ${_('Pull request #%s') % c.pull_request.pull_request_id}
9 </%def>
9 </%def>
10
10
11 <%def name="page_nav()">
11 <%def name="page_nav()">
12 ${self.menu('repositories')}
12 ${self.menu('repositories')}
13 </%def>
13 </%def>
14
14
15 <%def name="main()">
15 <%def name="main()">
16 ${self.context_bar('showpullrequest')}
16 ${self.context_bar('showpullrequest')}
17 <div class="box">
17 <div class="box">
18 <!-- box / title -->
18 <!-- box / title -->
19 <div class="title">
19 <div class="title">
20 ${self.breadcrumbs()}
20 ${self.breadcrumbs()}
21 </div>
21 </div>
22
22
23 <h3 class="${'closed' if c.pull_request.is_closed() else ''}">
23 <h3 class="${'closed' if c.pull_request.is_closed() else ''}">
24 <img src="${h.url('/images/icons/flag_status_%s.png' % str(c.pull_request.last_review_status))}" />
24 <img src="${h.url('/images/icons/flag_status_%s.png' % str(c.pull_request.last_review_status))}" />
25 ${_('Title')}: ${c.pull_request.title}
25 ${_('Title')}: ${c.pull_request.title}
26 %if c.pull_request.is_closed():
26 %if c.pull_request.is_closed():
27 (${_('Closed')})
27 (${_('Closed')})
28 %endif
28 %endif
29 </h3>
29 </h3>
30
30
31 <div class="form">
31 <div class="form">
32 <div id="summary" class="fields">
32 <div id="summary" class="fields">
33 <div class="field">
33 <div class="field">
34 <div class="label-summary">
34 <div class="label-summary">
35 <label>${_('Review status')}:</label>
35 <label>${_('Review status')}:</label>
36 </div>
36 </div>
37 <div class="input">
37 <div class="input">
38 <div class="changeset-status-container" style="float:none;clear:both">
38 <div class="changeset-status-container" style="float:none;clear:both">
39 %if c.current_changeset_status:
39 %if c.current_changeset_status:
40 <div title="${_('Pull request status')}" class="changeset-status-lbl">
40 <div title="${_('Pull request status')}" class="changeset-status-lbl">
41 %if c.pull_request.is_closed():
41 %if c.pull_request.is_closed():
42 ${_('Closed')},
42 ${_('Closed')},
43 %endif
43 %endif
44 ${h.changeset_status_lbl(c.current_changeset_status)}
44 ${h.changeset_status_lbl(c.current_changeset_status)}
45 </div>
45 </div>
46 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
46 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
47 %endif
47 %endif
48 </div>
48 </div>
49 </div>
49 </div>
50 </div>
50 </div>
51 <div class="field">
51 <div class="field">
52 <div class="label-summary">
52 <div class="label-summary">
53 <label>${_('Still not reviewed by')}:</label>
53 <label>${_('Still not reviewed by')}:</label>
54 </div>
54 </div>
55 <div class="input">
55 <div class="input">
56 % if len(c.pull_request_pending_reviewers) > 0:
56 % if len(c.pull_request_pending_reviewers) > 0:
57 <div class="tooltip" title="${h.tooltip(','.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
57 <div class="tooltip" title="${h.tooltip(', '.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
58 %else:
58 %else:
59 <div>${_('Pull request was reviewed by all reviewers')}</div>
59 <div>${_('Pull request was reviewed by all reviewers')}</div>
60 %endif
60 %endif
61 </div>
61 </div>
62 </div>
62 </div>
63 <div class="field">
63 <div class="field">
64 <div class="label-summary">
64 <div class="label-summary">
65 <label>${_('Origin repository')}:</label>
65 <label>${_('Origin repository')}:</label>
66 </div>
66 </div>
67 <div class="input">
67 <div class="input">
68 <div>
68 <div>
69 ##%if h.is_hg(c.pull_request.org_repo):
69 ##%if h.is_hg(c.pull_request.org_repo):
70 ## <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
70 ## <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
71 ##%elif h.is_git(c.pull_request.org_repo):
71 ##%elif h.is_git(c.pull_request.org_repo):
72 ## <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
72 ## <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
73 ##%endif
73 ##%endif
74 <span class="spantag">${c.pull_request.org_ref_parts[0]}: ${c.pull_request.org_ref_parts[1]}</span>
74 <span class="spantag">${c.pull_request.org_ref_parts[0]}: ${c.pull_request.org_ref_parts[1]}</span>
75 <span><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
75 <span><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
76 </div>
76 </div>
77 </div>
77 </div>
78 </div>
78 </div>
79 <div class="field">
79 <div class="field">
80 <div class="label-summary">
80 <div class="label-summary">
81 <label>${_('Description')}:</label>
81 <label>${_('Description')}:</label>
82 </div>
82 </div>
83 <div class="input">
83 <div class="input">
84 <div style="white-space:pre-wrap">${h.literal(c.pull_request.description)}</div>
84 <div style="white-space:pre-wrap">${h.literal(c.pull_request.description)}</div>
85 </div>
85 </div>
86 </div>
86 </div>
87 <div class="field">
87 <div class="field">
88 <div class="label-summary">
88 <div class="label-summary">
89 <label>${_('Created on')}:</label>
89 <label>${_('Created on')}:</label>
90 </div>
90 </div>
91 <div class="input">
91 <div class="input">
92 <div>${h.fmt_date(c.pull_request.created_on)}</div>
92 <div>${h.fmt_date(c.pull_request.created_on)}</div>
93 </div>
93 </div>
94 </div>
94 </div>
95 </div>
95 </div>
96 </div>
96 </div>
97
97
98 <div style="overflow: auto;">
98 <div style="overflow: auto;">
99 ##DIFF
99 ##DIFF
100 <div class="table" style="float:left;clear:none">
100 <div class="table" style="float:left;clear:none">
101 <div id="body" class="diffblock">
101 <div id="body" class="diffblock">
102 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
102 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
103 </div>
103 </div>
104 <div id="changeset_compare_view_content">
104 <div id="changeset_compare_view_content">
105 ##CS
105 ##CS
106 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
106 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
107 <%include file="/compare/compare_cs.html" />
107 <%include file="/compare/compare_cs.html" />
108
108
109 ## FILES
109 ## FILES
110 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
110 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
111
111
112 % if c.limited_diff:
112 % if c.limited_diff:
113 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
113 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
114 % else:
114 % else:
115 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
115 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
116 %endif
116 %endif
117
117
118 </div>
118 </div>
119 <div class="cs_files">
119 <div class="cs_files">
120 %if not c.files:
120 %if not c.files:
121 <span class="empty_data">${_('No files')}</span>
121 <span class="empty_data">${_('No files')}</span>
122 %endif
122 %endif
123 %for fid, change, f, stat in c.files:
123 %for fid, change, f, stat in c.files:
124 <div class="cs_${change}">
124 <div class="cs_${change}">
125 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
125 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
126 <div class="changes">${h.fancy_file_stats(stat)}</div>
126 <div class="changes">${h.fancy_file_stats(stat)}</div>
127 </div>
127 </div>
128 %endfor
128 %endfor
129 </div>
129 </div>
130 % if c.limited_diff:
130 % if c.limited_diff:
131 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
131 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
132 % endif
132 % endif
133 </div>
133 </div>
134 </div>
134 </div>
135 ## REVIEWERS
135 ## REVIEWERS
136 <div style="float:left; border-left:1px dashed #eee">
136 <div style="float:left; border-left:1px dashed #eee">
137 <h4>${_('Pull request reviewers')}</h4>
137 <h4>${_('Pull request reviewers')}</h4>
138 <div id="reviewers" style="padding:0px 0px 5px 10px">
138 <div id="reviewers" style="padding:0px 0px 5px 10px">
139 ## members goes here !
139 ## members goes here !
140 <div class="group_members_wrap" style="min-height:45px">
140 <div class="group_members_wrap" style="min-height:45px">
141 <ul id="review_members" class="group_members">
141 <ul id="review_members" class="group_members">
142 %for member,status in c.pull_request_reviewers:
142 %for member,status in c.pull_request_reviewers:
143 <li id="reviewer_${member.user_id}">
143 <li id="reviewer_${member.user_id}">
144 <div class="reviewers_member">
144 <div class="reviewers_member">
145 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
145 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
146 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
146 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
147 </div>
147 </div>
148 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
148 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
149 <div style="float:left">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
149 <div style="float:left">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
150 <input type="hidden" value="${member.user_id}" name="review_members" />
150 <input type="hidden" value="${member.user_id}" name="review_members" />
151 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.user_id == c.rhodecode_user.user_id):
151 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.user_id == c.rhodecode_user.user_id):
152 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
152 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
153 %endif
153 %endif
154 </div>
154 </div>
155 </li>
155 </li>
156 %endfor
156 %endfor
157 </ul>
157 </ul>
158 </div>
158 </div>
159 %if not c.pull_request.is_closed():
159 %if not c.pull_request.is_closed():
160 <div class='ac'>
160 <div class='ac'>
161 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
161 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
162 <div class="reviewer_ac">
162 <div class="reviewer_ac">
163 ${h.text('user', class_='yui-ac-input')}
163 ${h.text('user', class_='yui-ac-input')}
164 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
164 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
165 <div id="reviewers_container"></div>
165 <div id="reviewers_container"></div>
166 </div>
166 </div>
167 <div style="padding:0px 10px">
167 <div style="padding:0px 10px">
168 <span id="update_pull_request" class="ui-btn xsmall">${_('Save changes')}</span>
168 <span id="update_pull_request" class="ui-btn xsmall">${_('Save changes')}</span>
169 </div>
169 </div>
170 %endif
170 %endif
171 </div>
171 </div>
172 %endif
172 %endif
173 </div>
173 </div>
174 </div>
174 </div>
175 </div>
175 </div>
176 <script>
176 <script>
177 var _USERS_AC_DATA = ${c.users_array|n};
177 var _USERS_AC_DATA = ${c.users_array|n};
178 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
178 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
179 // TODO: switch this to pyroutes
179 // TODO: switch this to pyroutes
180 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
180 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
181 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
181 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
182
182
183 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
183 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
184 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
184 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
185 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
185 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
186
186
187 </script>
187 </script>
188
188
189 ## diff block
189 ## diff block
190 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
190 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
191 %for fid, change, f, stat in c.files:
191 %for fid, change, f, stat in c.files:
192 ${diff_block.diff_block_simple([c.changes[fid]])}
192 ${diff_block.diff_block_simple([c.changes[fid]])}
193 %endfor
193 %endfor
194 % if c.limited_diff:
194 % if c.limited_diff:
195 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h4>
195 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h4>
196 % endif
196 % endif
197
197
198
198
199 ## template for inline comment form
199 ## template for inline comment form
200 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
200 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
201 ${comment.comment_inline_form()}
201 ${comment.comment_inline_form()}
202
202
203 ## render comments and inlines
203 ## render comments and inlines
204 ${comment.generate_comments(include_pr=True)}
204 ${comment.generate_comments(include_pr=True)}
205
205
206 % if not c.pull_request.is_closed():
206 % if not c.pull_request.is_closed():
207 ## main comment form and it status
207 ## main comment form and it status
208 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
208 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
209 pull_request_id=c.pull_request.pull_request_id),
209 pull_request_id=c.pull_request.pull_request_id),
210 c.current_changeset_status,
210 c.current_changeset_status,
211 close_btn=True, change_status=c.allowed_to_change_status)}
211 close_btn=True, change_status=c.allowed_to_change_status)}
212 %endif
212 %endif
213
213
214 <script type="text/javascript">
214 <script type="text/javascript">
215 YUE.onDOMReady(function(){
215 YUE.onDOMReady(function(){
216 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
216 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
217
217
218 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
218 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
219 var show = 'none';
219 var show = 'none';
220 var target = e.currentTarget;
220 var target = e.currentTarget;
221 if(target.checked){
221 if(target.checked){
222 var show = ''
222 var show = ''
223 }
223 }
224 var boxid = YUD.getAttribute(target,'id_for');
224 var boxid = YUD.getAttribute(target,'id_for');
225 var comments = YUQ('#{0} .inline-comments'.format(boxid));
225 var comments = YUQ('#{0} .inline-comments'.format(boxid));
226 for(c in comments){
226 for(c in comments){
227 YUD.setStyle(comments[c],'display',show);
227 YUD.setStyle(comments[c],'display',show);
228 }
228 }
229 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
229 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
230 for(c in btns){
230 for(c in btns){
231 YUD.setStyle(btns[c],'display',show);
231 YUD.setStyle(btns[c],'display',show);
232 }
232 }
233 })
233 })
234
234
235 YUE.on(YUQ('.line'),'click',function(e){
235 YUE.on(YUQ('.line'),'click',function(e){
236 var tr = e.currentTarget;
236 var tr = e.currentTarget;
237 injectInlineForm(tr);
237 injectInlineForm(tr);
238 });
238 });
239
239
240 // inject comments into they proper positions
240 // inject comments into they proper positions
241 var file_comments = YUQ('.inline-comment-placeholder');
241 var file_comments = YUQ('.inline-comment-placeholder');
242 renderInlineComments(file_comments);
242 renderInlineComments(file_comments);
243
243
244 YUE.on(YUD.get('update_pull_request'),'click',function(e){
244 YUE.on(YUD.get('update_pull_request'),'click',function(e){
245 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
245 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
246 })
246 })
247 })
247 })
248 </script>
248 </script>
249
249
250 </div>
250 </div>
251
251
252 </%def>
252 </%def>
General Comments 0
You need to be logged in to leave comments. Login now