##// END OF EJS Templates
show only open pull requests in the counter, and use repo context bar in pull requests view
marcink -
r3580:7b9d4f6b beta
parent child Browse files
Show More
@@ -1,338 +1,337 b''
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 6 import time
7 7 import traceback
8 8
9 9 from paste.auth.basic import AuthBasicAuthenticator
10 10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
11 11 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
12 12
13 13 from pylons import config, tmpl_context as c, request, session, url
14 14 from pylons.controllers import WSGIController
15 15 from pylons.controllers.util import redirect
16 16 from pylons.templating import render_mako as render
17 17
18 18 from rhodecode import __version__, BACKENDS
19 19
20 20 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
21 21 safe_str, safe_int
22 22 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
23 23 HasPermissionAnyMiddleware, CookieStoreWrapper
24 24 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
25 25 from rhodecode.model import meta
26 26
27 27 from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting
28 28 from rhodecode.model.notification import NotificationModel
29 29 from rhodecode.model.scm import ScmModel
30 30 from rhodecode.model.meta import Session
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 def _get_ip_addr(environ):
36 36 proxy_key = 'HTTP_X_REAL_IP'
37 37 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
38 38 def_key = 'REMOTE_ADDR'
39 39
40 40 ip = environ.get(proxy_key)
41 41 if ip:
42 42 return ip
43 43
44 44 ip = environ.get(proxy_key2)
45 45 if ip:
46 46 return ip
47 47
48 48 ip = environ.get(def_key, '0.0.0.0')
49 49
50 50 # HEADERS can have mutliple ips inside
51 51 # the left-most being the original client, and each successive proxy
52 52 # that passed the request adding the IP address where it received the
53 53 # request from.
54 54 if ',' in ip:
55 55 ip = ip.split(',')[0].strip()
56 56
57 57 return ip
58 58
59 59
60 60 def _get_access_path(environ):
61 61 path = environ.get('PATH_INFO')
62 62 org_req = environ.get('pylons.original_request')
63 63 if org_req:
64 64 path = org_req.environ.get('PATH_INFO')
65 65 return path
66 66
67 67
68 68 class BasicAuth(AuthBasicAuthenticator):
69 69
70 70 def __init__(self, realm, authfunc, auth_http_code=None):
71 71 self.realm = realm
72 72 self.authfunc = authfunc
73 73 self._rc_auth_http_code = auth_http_code
74 74
75 75 def build_authentication(self):
76 76 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
77 77 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
78 78 # return 403 if alternative http return code is specified in
79 79 # RhodeCode config
80 80 return HTTPForbidden(headers=head)
81 81 return HTTPUnauthorized(headers=head)
82 82
83 83 def authenticate(self, environ):
84 84 authorization = AUTHORIZATION(environ)
85 85 if not authorization:
86 86 return self.build_authentication()
87 87 (authmeth, auth) = authorization.split(' ', 1)
88 88 if 'basic' != authmeth.lower():
89 89 return self.build_authentication()
90 90 auth = auth.strip().decode('base64')
91 91 _parts = auth.split(':', 1)
92 92 if len(_parts) == 2:
93 93 username, password = _parts
94 94 if self.authfunc(environ, username, password):
95 95 return username
96 96 return self.build_authentication()
97 97
98 98 __call__ = authenticate
99 99
100 100
101 101 class BaseVCSController(object):
102 102
103 103 def __init__(self, application, config):
104 104 self.application = application
105 105 self.config = config
106 106 # base path of repo locations
107 107 self.basepath = self.config['base_path']
108 108 #authenticate this mercurial request using authfunc
109 109 self.authenticate = BasicAuth('', authfunc,
110 110 config.get('auth_ret_code'))
111 111 self.ip_addr = '0.0.0.0'
112 112
113 113 def _handle_request(self, environ, start_response):
114 114 raise NotImplementedError()
115 115
116 116 def _get_by_id(self, repo_name):
117 117 """
118 118 Get's a special pattern _<ID> from clone url and tries to replace it
119 119 with a repository_name for support of _<ID> non changable urls
120 120
121 121 :param repo_name:
122 122 """
123 123 try:
124 124 data = repo_name.split('/')
125 125 if len(data) >= 2:
126 126 by_id = data[1].split('_')
127 127 if len(by_id) == 2 and by_id[1].isdigit():
128 128 _repo_name = Repository.get(by_id[1]).repo_name
129 129 data[1] = _repo_name
130 130 except:
131 131 log.debug('Failed to extract repo_name from id %s' % (
132 132 traceback.format_exc()
133 133 )
134 134 )
135 135
136 136 return '/'.join(data)
137 137
138 138 def _invalidate_cache(self, repo_name):
139 139 """
140 140 Set's cache for this repository for invalidation on next access
141 141
142 142 :param repo_name: full repo name, also a cache key
143 143 """
144 144 invalidate_cache('get_repo_cached_%s' % repo_name)
145 145
146 146 def _check_permission(self, action, user, repo_name, ip_addr=None):
147 147 """
148 148 Checks permissions using action (push/pull) user and repository
149 149 name
150 150
151 151 :param action: push or pull action
152 152 :param user: user instance
153 153 :param repo_name: repository name
154 154 """
155 155 #check IP
156 156 authuser = AuthUser(user_id=user.user_id, ip_addr=ip_addr)
157 157 if not authuser.ip_allowed:
158 158 return False
159 159 else:
160 160 log.info('Access for IP:%s allowed' % (ip_addr))
161 161 if action == 'push':
162 162 if not HasPermissionAnyMiddleware('repository.write',
163 163 'repository.admin')(user,
164 164 repo_name):
165 165 return False
166 166
167 167 else:
168 168 #any other action need at least read permission
169 169 if not HasPermissionAnyMiddleware('repository.read',
170 170 'repository.write',
171 171 'repository.admin')(user,
172 172 repo_name):
173 173 return False
174 174
175 175 return True
176 176
177 177 def _get_ip_addr(self, environ):
178 178 return _get_ip_addr(environ)
179 179
180 180 def _check_ssl(self, environ, start_response):
181 181 """
182 182 Checks the SSL check flag and returns False if SSL is not present
183 183 and required True otherwise
184 184 """
185 185 org_proto = environ['wsgi._org_proto']
186 186 #check if we have SSL required ! if not it's a bad request !
187 187 require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl').ui_value)
188 188 if require_ssl and org_proto == 'http':
189 189 log.debug('proto is %s and SSL is required BAD REQUEST !'
190 190 % org_proto)
191 191 return False
192 192 return True
193 193
194 194 def _check_locking_state(self, environ, action, repo, user_id):
195 195 """
196 196 Checks locking on this repository, if locking is enabled and lock is
197 197 present returns a tuple of make_lock, locked, locked_by.
198 198 make_lock can have 3 states None (do nothing) True, make lock
199 199 False release lock, This value is later propagated to hooks, which
200 200 do the locking. Think about this as signals passed to hooks what to do.
201 201
202 202 """
203 203 locked = False # defines that locked error should be thrown to user
204 204 make_lock = None
205 205 repo = Repository.get_by_repo_name(repo)
206 206 user = User.get(user_id)
207 207
208 208 # this is kind of hacky, but due to how mercurial handles client-server
209 209 # server see all operation on changeset; bookmarks, phases and
210 210 # obsolescence marker in different transaction, we don't want to check
211 211 # locking on those
212 212 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
213 213 locked_by = repo.locked
214 214 if repo and repo.enable_locking and not obsolete_call:
215 215 if action == 'push':
216 216 #check if it's already locked !, if it is compare users
217 217 user_id, _date = repo.locked
218 218 if user.user_id == user_id:
219 219 log.debug('Got push from user %s, now unlocking' % (user))
220 220 # unlock if we have push from user who locked
221 221 make_lock = False
222 222 else:
223 223 # we're not the same user who locked, ban with 423 !
224 224 locked = True
225 225 if action == 'pull':
226 226 if repo.locked[0] and repo.locked[1]:
227 227 locked = True
228 228 else:
229 229 log.debug('Setting lock on repo %s by %s' % (repo, user))
230 230 make_lock = True
231 231
232 232 else:
233 233 log.debug('Repository %s do not have locking enabled' % (repo))
234 234 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
235 235 % (make_lock, locked, locked_by))
236 236 return make_lock, locked, locked_by
237 237
238 238 def __call__(self, environ, start_response):
239 239 start = time.time()
240 240 try:
241 241 return self._handle_request(environ, start_response)
242 242 finally:
243 243 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
244 244 log.debug('Request time: %.3fs' % (time.time() - start))
245 245 meta.Session.remove()
246 246
247 247
248 248 class BaseController(WSGIController):
249 249
250 250 def __before__(self):
251 251 """
252 252 __before__ is called before controller methods and after __call__
253 253 """
254 254 c.rhodecode_version = __version__
255 255 c.rhodecode_instanceid = config.get('instance_id')
256 256 c.rhodecode_name = config.get('rhodecode_title')
257 257 c.use_gravatar = str2bool(config.get('use_gravatar'))
258 258 c.ga_code = config.get('rhodecode_ga_code')
259 259 # Visual options
260 260 c.visual = AttributeDict({})
261 261 rc_config = RhodeCodeSetting.get_app_settings()
262 262
263 263 c.visual.show_public_icon = str2bool(rc_config.get('rhodecode_show_public_icon'))
264 264 c.visual.show_private_icon = str2bool(rc_config.get('rhodecode_show_private_icon'))
265 265 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
266 266 c.visual.lightweight_dashboard = str2bool(rc_config.get('rhodecode_lightweight_dashboard'))
267 267 c.visual.lightweight_dashboard_items = safe_int(config.get('dashboard_items', 100))
268 268 c.visual.repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
269
270 c.repo_name = get_repo_slug(request)
269 c.repo_name = get_repo_slug(request) # can be empty
271 270 c.backends = BACKENDS.keys()
272 271 c.unread_notifications = NotificationModel()\
273 272 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
274 273 self.cut_off_limit = int(config.get('cut_off_limit'))
275 274
276 275 self.sa = meta.Session
277 276 self.scm_model = ScmModel(self.sa)
278 277
279 278 def __call__(self, environ, start_response):
280 279 """Invoke the Controller"""
281 280 # WSGIController.__call__ dispatches to the Controller method
282 281 # the request is routed to. This routing information is
283 282 # available in environ['pylons.routes_dict']
284 283 try:
285 284 self.ip_addr = _get_ip_addr(environ)
286 285 # make sure that we update permissions each time we call controller
287 286 api_key = request.GET.get('api_key')
288 287 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
289 288 user_id = cookie_store.get('user_id', None)
290 289 username = get_container_username(environ, config)
291 290 auth_user = AuthUser(user_id, api_key, username, self.ip_addr)
292 291 request.user = auth_user
293 292 self.rhodecode_user = c.rhodecode_user = auth_user
294 293 if not self.rhodecode_user.is_authenticated and \
295 294 self.rhodecode_user.user_id is not None:
296 295 self.rhodecode_user.set_authenticated(
297 296 cookie_store.get('is_authenticated')
298 297 )
299 298 log.info('IP: %s User: %s accessed %s' % (
300 299 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
301 300 )
302 301 return WSGIController.__call__(self, environ, start_response)
303 302 finally:
304 303 meta.Session.remove()
305 304
306 305
307 306 class BaseRepoController(BaseController):
308 307 """
309 308 Base class for controllers responsible for loading all needed data for
310 309 repository loaded items are
311 310
312 311 c.rhodecode_repo: instance of scm repository
313 312 c.rhodecode_db_repo: instance of db
314 313 c.repository_followers: number of followers
315 314 c.repository_forks: number of forks
316 315 c.repository_following: weather the current user is following the current repo
317 316 """
318 317
319 318 def __before__(self):
320 319 super(BaseRepoController, self).__before__()
321 320 if c.repo_name:
322 321
323 322 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
324 323 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
325 324 # update last change according to VCS data
326 325 dbr.update_changeset_cache(dbr.get_changeset())
327 326 if c.rhodecode_repo is None:
328 327 log.error('%s this repository is present in database but it '
329 328 'cannot be created as an scm instance', c.repo_name)
330 329
331 330 redirect(url('home'))
332 331
333 332 # some globals counter for menu
334 333 c.repository_followers = self.scm_model.get_followers(dbr)
335 334 c.repository_forks = self.scm_model.get_forks(dbr)
336 335 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
337 336 c.repository_following = self.scm_model.is_following_repo(c.repo_name,
338 337 self.rhodecode_user.user_id)
@@ -1,673 +1,674 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
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 from __future__ import with_statement
26 26 import os
27 27 import re
28 28 import time
29 29 import traceback
30 30 import logging
31 31 import cStringIO
32 32 import pkg_resources
33 33 from os.path import dirname as dn, join as jn
34 34
35 35 from sqlalchemy import func
36 36 from pylons.i18n.translation import _
37 37
38 38 import rhodecode
39 39 from rhodecode.lib.vcs import get_backend
40 40 from rhodecode.lib.vcs.exceptions import RepositoryError
41 41 from rhodecode.lib.vcs.utils.lazy import LazyProperty
42 42 from rhodecode.lib.vcs.nodes import FileNode
43 43 from rhodecode.lib.vcs.backends.base import EmptyChangeset
44 44
45 45 from rhodecode import BACKENDS
46 46 from rhodecode.lib import helpers as h
47 47 from rhodecode.lib.utils2 import safe_str, safe_unicode, get_server_url,\
48 48 _set_extras
49 49 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
50 50 from rhodecode.lib.utils import get_filesystem_repos, make_ui, \
51 51 action_logger, REMOVED_REPO_PAT
52 52 from rhodecode.model import BaseModel
53 53 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
54 54 UserFollowing, UserLog, User, RepoGroup, PullRequest
55 55 from rhodecode.lib.hooks import log_push_action
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class UserTemp(object):
61 61 def __init__(self, user_id):
62 62 self.user_id = user_id
63 63
64 64 def __repr__(self):
65 65 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
66 66
67 67
68 68 class RepoTemp(object):
69 69 def __init__(self, repo_id):
70 70 self.repo_id = repo_id
71 71
72 72 def __repr__(self):
73 73 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
74 74
75 75
76 76 class CachedRepoList(object):
77 77 """
78 78 Cached repo list, uses in-memory cache after initialization, that is
79 79 super fast
80 80 """
81 81
82 82 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
83 83 self.db_repo_list = db_repo_list
84 84 self.repos_path = repos_path
85 85 self.order_by = order_by
86 86 self.reversed = (order_by or '').startswith('-')
87 87 if not perm_set:
88 88 perm_set = ['repository.read', 'repository.write',
89 89 'repository.admin']
90 90 self.perm_set = perm_set
91 91
92 92 def __len__(self):
93 93 return len(self.db_repo_list)
94 94
95 95 def __repr__(self):
96 96 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
97 97
98 98 def __iter__(self):
99 99 # pre-propagated cache_map to save executing select statements
100 100 # for each repo
101 101 cache_map = CacheInvalidation.get_cache_map()
102 102
103 103 for dbr in self.db_repo_list:
104 104 scmr = dbr.scm_instance_cached(cache_map)
105 105 # check permission at this level
106 106 if not HasRepoPermissionAny(
107 107 *self.perm_set
108 108 )(dbr.repo_name, 'get repo check'):
109 109 continue
110 110
111 111 try:
112 112 last_change = scmr.last_change
113 113 tip = h.get_changeset_safe(scmr, 'tip')
114 114 except Exception:
115 115 log.error(
116 116 '%s this repository is present in database but it '
117 117 'cannot be created as an scm instance, org_exc:%s'
118 118 % (dbr.repo_name, traceback.format_exc())
119 119 )
120 120 continue
121 121
122 122 tmp_d = {}
123 123 tmp_d['name'] = dbr.repo_name
124 124 tmp_d['name_sort'] = tmp_d['name'].lower()
125 125 tmp_d['raw_name'] = tmp_d['name'].lower()
126 126 tmp_d['description'] = dbr.description
127 127 tmp_d['description_sort'] = tmp_d['description'].lower()
128 128 tmp_d['last_change'] = last_change
129 129 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
130 130 tmp_d['tip'] = tip.raw_id
131 131 tmp_d['tip_sort'] = tip.revision
132 132 tmp_d['rev'] = tip.revision
133 133 tmp_d['contact'] = dbr.user.full_contact
134 134 tmp_d['contact_sort'] = tmp_d['contact']
135 135 tmp_d['owner_sort'] = tmp_d['contact']
136 136 tmp_d['repo_archives'] = list(scmr._get_archives())
137 137 tmp_d['last_msg'] = tip.message
138 138 tmp_d['author'] = tip.author
139 139 tmp_d['dbrepo'] = dbr.get_dict()
140 140 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
141 141 yield tmp_d
142 142
143 143
144 144 class SimpleCachedRepoList(CachedRepoList):
145 145 """
146 146 Lighter version of CachedRepoList without the scm initialisation
147 147 """
148 148
149 149 def __iter__(self):
150 150 for dbr in self.db_repo_list:
151 151 # check permission at this level
152 152 if not HasRepoPermissionAny(
153 153 *self.perm_set
154 154 )(dbr.repo_name, 'get repo check'):
155 155 continue
156 156
157 157 tmp_d = {}
158 158 tmp_d['name'] = dbr.repo_name
159 159 tmp_d['name_sort'] = tmp_d['name'].lower()
160 160 tmp_d['raw_name'] = tmp_d['name'].lower()
161 161 tmp_d['description'] = dbr.description
162 162 tmp_d['description_sort'] = tmp_d['description'].lower()
163 163 tmp_d['dbrepo'] = dbr.get_dict()
164 164 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
165 165 yield tmp_d
166 166
167 167
168 168 class GroupList(object):
169 169
170 170 def __init__(self, db_repo_group_list, perm_set=None):
171 171 """
172 172 Creates iterator from given list of group objects, additionally
173 173 checking permission for them from perm_set var
174 174
175 175 :param db_repo_group_list:
176 176 :param perm_set: list of permissons to check
177 177 """
178 178 self.db_repo_group_list = db_repo_group_list
179 179 if not perm_set:
180 180 perm_set = ['group.read', 'group.write', 'group.admin']
181 181 self.perm_set = perm_set
182 182
183 183 def __len__(self):
184 184 return len(self.db_repo_group_list)
185 185
186 186 def __repr__(self):
187 187 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
188 188
189 189 def __iter__(self):
190 190 for dbgr in self.db_repo_group_list:
191 191 # check permission at this level
192 192 if not HasReposGroupPermissionAny(
193 193 *self.perm_set
194 194 )(dbgr.group_name, 'get group repo check'):
195 195 continue
196 196
197 197 yield dbgr
198 198
199 199
200 200 class ScmModel(BaseModel):
201 201 """
202 202 Generic Scm Model
203 203 """
204 204
205 205 def __get_repo(self, instance):
206 206 cls = Repository
207 207 if isinstance(instance, cls):
208 208 return instance
209 209 elif isinstance(instance, int) or safe_str(instance).isdigit():
210 210 return cls.get(instance)
211 211 elif isinstance(instance, basestring):
212 212 return cls.get_by_repo_name(instance)
213 213 elif instance:
214 214 raise Exception('given object must be int, basestr or Instance'
215 215 ' of %s got %s' % (type(cls), type(instance)))
216 216
217 217 @LazyProperty
218 218 def repos_path(self):
219 219 """
220 220 Get's the repositories root path from database
221 221 """
222 222
223 223 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
224 224
225 225 return q.ui_value
226 226
227 227 def repo_scan(self, repos_path=None):
228 228 """
229 229 Listing of repositories in given path. This path should not be a
230 230 repository itself. Return a dictionary of repository objects
231 231
232 232 :param repos_path: path to directory containing repositories
233 233 """
234 234
235 235 if repos_path is None:
236 236 repos_path = self.repos_path
237 237
238 238 log.info('scanning for repositories in %s' % repos_path)
239 239
240 240 baseui = make_ui('db')
241 241 repos = {}
242 242
243 243 for name, path in get_filesystem_repos(repos_path, recursive=True):
244 244 # name need to be decomposed and put back together using the /
245 245 # since this is internal storage separator for rhodecode
246 246 name = Repository.normalize_repo_name(name)
247 247
248 248 try:
249 249 if name in repos:
250 250 raise RepositoryError('Duplicate repository name %s '
251 251 'found in %s' % (name, path))
252 252 else:
253 253
254 254 klass = get_backend(path[0])
255 255
256 256 if path[0] == 'hg' and path[0] in BACKENDS.keys():
257 257 repos[name] = klass(safe_str(path[1]), baseui=baseui)
258 258
259 259 if path[0] == 'git' and path[0] in BACKENDS.keys():
260 260 repos[name] = klass(path[1])
261 261 except OSError:
262 262 continue
263 263 log.debug('found %s paths with repositories' % (len(repos)))
264 264 return repos
265 265
266 266 def get_repos(self, all_repos=None, sort_key=None, simple=False):
267 267 """
268 268 Get all repos from db and for each repo create it's
269 269 backend instance and fill that backed with information from database
270 270
271 271 :param all_repos: list of repository names as strings
272 272 give specific repositories list, good for filtering
273 273
274 274 :param sort_key: initial sorting of repos
275 275 :param simple: use SimpleCachedList - one without the SCM info
276 276 """
277 277 if all_repos is None:
278 278 all_repos = self.sa.query(Repository)\
279 279 .filter(Repository.group_id == None)\
280 280 .order_by(func.lower(Repository.repo_name)).all()
281 281 if simple:
282 282 repo_iter = SimpleCachedRepoList(all_repos,
283 283 repos_path=self.repos_path,
284 284 order_by=sort_key)
285 285 else:
286 286 repo_iter = CachedRepoList(all_repos,
287 287 repos_path=self.repos_path,
288 288 order_by=sort_key)
289 289
290 290 return repo_iter
291 291
292 292 def get_repos_groups(self, all_groups=None):
293 293 if all_groups is None:
294 294 all_groups = RepoGroup.query()\
295 295 .filter(RepoGroup.group_parent_id == None).all()
296 296 return [x for x in GroupList(all_groups)]
297 297
298 298 def mark_for_invalidation(self, repo_name):
299 299 """
300 300 Puts cache invalidation task into db for
301 301 further global cache invalidation
302 302
303 303 :param repo_name: this repo that should invalidation take place
304 304 """
305 305 invalidated_keys = CacheInvalidation.set_invalidate(repo_name=repo_name)
306 306 repo = Repository.get_by_repo_name(repo_name)
307 307 if repo:
308 308 repo.update_changeset_cache()
309 309 return invalidated_keys
310 310
311 311 def toggle_following_repo(self, follow_repo_id, user_id):
312 312
313 313 f = self.sa.query(UserFollowing)\
314 314 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
315 315 .filter(UserFollowing.user_id == user_id).scalar()
316 316
317 317 if f is not None:
318 318 try:
319 319 self.sa.delete(f)
320 320 action_logger(UserTemp(user_id),
321 321 'stopped_following_repo',
322 322 RepoTemp(follow_repo_id))
323 323 return
324 324 except:
325 325 log.error(traceback.format_exc())
326 326 raise
327 327
328 328 try:
329 329 f = UserFollowing()
330 330 f.user_id = user_id
331 331 f.follows_repo_id = follow_repo_id
332 332 self.sa.add(f)
333 333
334 334 action_logger(UserTemp(user_id),
335 335 'started_following_repo',
336 336 RepoTemp(follow_repo_id))
337 337 except:
338 338 log.error(traceback.format_exc())
339 339 raise
340 340
341 341 def toggle_following_user(self, follow_user_id, user_id):
342 342 f = self.sa.query(UserFollowing)\
343 343 .filter(UserFollowing.follows_user_id == follow_user_id)\
344 344 .filter(UserFollowing.user_id == user_id).scalar()
345 345
346 346 if f is not None:
347 347 try:
348 348 self.sa.delete(f)
349 349 return
350 350 except:
351 351 log.error(traceback.format_exc())
352 352 raise
353 353
354 354 try:
355 355 f = UserFollowing()
356 356 f.user_id = user_id
357 357 f.follows_user_id = follow_user_id
358 358 self.sa.add(f)
359 359 except:
360 360 log.error(traceback.format_exc())
361 361 raise
362 362
363 363 def is_following_repo(self, repo_name, user_id, cache=False):
364 364 r = self.sa.query(Repository)\
365 365 .filter(Repository.repo_name == repo_name).scalar()
366 366
367 367 f = self.sa.query(UserFollowing)\
368 368 .filter(UserFollowing.follows_repository == r)\
369 369 .filter(UserFollowing.user_id == user_id).scalar()
370 370
371 371 return f is not None
372 372
373 373 def is_following_user(self, username, user_id, cache=False):
374 374 u = User.get_by_username(username)
375 375
376 376 f = self.sa.query(UserFollowing)\
377 377 .filter(UserFollowing.follows_user == u)\
378 378 .filter(UserFollowing.user_id == user_id).scalar()
379 379
380 380 return f is not None
381 381
382 382 def get_followers(self, repo):
383 383 repo = self._get_repo(repo)
384 384
385 385 return self.sa.query(UserFollowing)\
386 386 .filter(UserFollowing.follows_repository == repo).count()
387 387
388 388 def get_forks(self, repo):
389 389 repo = self._get_repo(repo)
390 390 return self.sa.query(Repository)\
391 391 .filter(Repository.fork == repo).count()
392 392
393 393 def get_pull_requests(self, repo):
394 394 repo = self._get_repo(repo)
395 395 return self.sa.query(PullRequest)\
396 .filter(PullRequest.other_repo == repo).count()
396 .filter(PullRequest.other_repo == repo)\
397 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
397 398
398 399 def mark_as_fork(self, repo, fork, user):
399 400 repo = self.__get_repo(repo)
400 401 fork = self.__get_repo(fork)
401 402 if fork and repo.repo_id == fork.repo_id:
402 403 raise Exception("Cannot set repository as fork of itself")
403 404 repo.fork = fork
404 405 self.sa.add(repo)
405 406 return repo
406 407
407 408 def _handle_push(self, repo, username, action, repo_name, revisions):
408 409 """
409 410 Triggers push action hooks
410 411
411 412 :param repo: SCM repo
412 413 :param username: username who pushes
413 414 :param action: push/push_loca/push_remote
414 415 :param repo_name: name of repo
415 416 :param revisions: list of revisions that we pushed
416 417 """
417 418 from rhodecode import CONFIG
418 419 from rhodecode.lib.base import _get_ip_addr
419 420 try:
420 421 from pylons import request
421 422 environ = request.environ
422 423 except TypeError:
423 424 # we might use this outside of request context, let's fake the
424 425 # environ data
425 426 from webob import Request
426 427 environ = Request.blank('').environ
427 428
428 429 #trigger push hook
429 430 extras = {
430 431 'ip': _get_ip_addr(environ),
431 432 'username': username,
432 433 'action': 'push_local',
433 434 'repository': repo_name,
434 435 'scm': repo.alias,
435 436 'config': CONFIG['__file__'],
436 437 'server_url': get_server_url(environ),
437 438 'make_lock': None,
438 439 'locked_by': [None, None]
439 440 }
440 441 _scm_repo = repo._repo
441 442 _set_extras(extras)
442 443 if repo.alias == 'hg':
443 444 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
444 445 elif repo.alias == 'git':
445 446 log_push_action(_scm_repo.ui, _scm_repo, _git_revs=revisions)
446 447
447 448 def _get_IMC_module(self, scm_type):
448 449 """
449 450 Returns InMemoryCommit class based on scm_type
450 451
451 452 :param scm_type:
452 453 """
453 454 if scm_type == 'hg':
454 455 from rhodecode.lib.vcs.backends.hg import \
455 456 MercurialInMemoryChangeset as IMC
456 457 elif scm_type == 'git':
457 458 from rhodecode.lib.vcs.backends.git import \
458 459 GitInMemoryChangeset as IMC
459 460 return IMC
460 461
461 462 def pull_changes(self, repo, username):
462 463 dbrepo = self.__get_repo(repo)
463 464 clone_uri = dbrepo.clone_uri
464 465 if not clone_uri:
465 466 raise Exception("This repository doesn't have a clone uri")
466 467
467 468 repo = dbrepo.scm_instance
468 469 repo_name = dbrepo.repo_name
469 470 try:
470 471 if repo.alias == 'git':
471 472 repo.fetch(clone_uri)
472 473 else:
473 474 repo.pull(clone_uri)
474 475 self.mark_for_invalidation(repo_name)
475 476 except:
476 477 log.error(traceback.format_exc())
477 478 raise
478 479
479 480 def commit_change(self, repo, repo_name, cs, user, author, message,
480 481 content, f_path):
481 482 """
482 483 Commits changes
483 484
484 485 :param repo: SCM instance
485 486
486 487 """
487 488 user = self._get_user(user)
488 489 IMC = self._get_IMC_module(repo.alias)
489 490
490 491 # decoding here will force that we have proper encoded values
491 492 # in any other case this will throw exceptions and deny commit
492 493 content = safe_str(content)
493 494 path = safe_str(f_path)
494 495 # message and author needs to be unicode
495 496 # proper backend should then translate that into required type
496 497 message = safe_unicode(message)
497 498 author = safe_unicode(author)
498 499 m = IMC(repo)
499 500 m.change(FileNode(path, content))
500 501 tip = m.commit(message=message,
501 502 author=author,
502 503 parents=[cs], branch=cs.branch)
503 504
504 505 self.mark_for_invalidation(repo_name)
505 506 self._handle_push(repo,
506 507 username=user.username,
507 508 action='push_local',
508 509 repo_name=repo_name,
509 510 revisions=[tip.raw_id])
510 511 return tip
511 512
512 513 def create_node(self, repo, repo_name, cs, user, author, message, content,
513 514 f_path):
514 515 user = self._get_user(user)
515 516 IMC = self._get_IMC_module(repo.alias)
516 517
517 518 # decoding here will force that we have proper encoded values
518 519 # in any other case this will throw exceptions and deny commit
519 520 if isinstance(content, (basestring,)):
520 521 content = safe_str(content)
521 522 elif isinstance(content, (file, cStringIO.OutputType,)):
522 523 content = content.read()
523 524 else:
524 525 raise Exception('Content is of unrecognized type %s' % (
525 526 type(content)
526 527 ))
527 528
528 529 message = safe_unicode(message)
529 530 author = safe_unicode(author)
530 531 path = safe_str(f_path)
531 532 m = IMC(repo)
532 533
533 534 if isinstance(cs, EmptyChangeset):
534 535 # EmptyChangeset means we we're editing empty repository
535 536 parents = None
536 537 else:
537 538 parents = [cs]
538 539
539 540 m.add(FileNode(path, content=content))
540 541 tip = m.commit(message=message,
541 542 author=author,
542 543 parents=parents, branch=cs.branch)
543 544
544 545 self.mark_for_invalidation(repo_name)
545 546 self._handle_push(repo,
546 547 username=user.username,
547 548 action='push_local',
548 549 repo_name=repo_name,
549 550 revisions=[tip.raw_id])
550 551 return tip
551 552
552 553 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
553 554 """
554 555 recursive walk in root dir and return a set of all path in that dir
555 556 based on repository walk function
556 557
557 558 :param repo_name: name of repository
558 559 :param revision: revision for which to list nodes
559 560 :param root_path: root path to list
560 561 :param flat: return as a list, if False returns a dict with decription
561 562
562 563 """
563 564 _files = list()
564 565 _dirs = list()
565 566 try:
566 567 _repo = self.__get_repo(repo_name)
567 568 changeset = _repo.scm_instance.get_changeset(revision)
568 569 root_path = root_path.lstrip('/')
569 570 for topnode, dirs, files in changeset.walk(root_path):
570 571 for f in files:
571 572 _files.append(f.path if flat else {"name": f.path,
572 573 "type": "file"})
573 574 for d in dirs:
574 575 _dirs.append(d.path if flat else {"name": d.path,
575 576 "type": "dir"})
576 577 except RepositoryError:
577 578 log.debug(traceback.format_exc())
578 579 raise
579 580
580 581 return _dirs, _files
581 582
582 583 def get_unread_journal(self):
583 584 return self.sa.query(UserLog).count()
584 585
585 586 def get_repo_landing_revs(self, repo=None):
586 587 """
587 588 Generates select option with tags branches and bookmarks (for hg only)
588 589 grouped by type
589 590
590 591 :param repo:
591 592 :type repo:
592 593 """
593 594
594 595 hist_l = []
595 596 choices = []
596 597 repo = self.__get_repo(repo)
597 598 hist_l.append(['tip', _('latest tip')])
598 599 choices.append('tip')
599 600 if not repo:
600 601 return choices, hist_l
601 602
602 603 repo = repo.scm_instance
603 604
604 605 branches_group = ([(k, k) for k, v in
605 606 repo.branches.iteritems()], _("Branches"))
606 607 hist_l.append(branches_group)
607 608 choices.extend([x[0] for x in branches_group[0]])
608 609
609 610 if repo.alias == 'hg':
610 611 bookmarks_group = ([(k, k) for k, v in
611 612 repo.bookmarks.iteritems()], _("Bookmarks"))
612 613 hist_l.append(bookmarks_group)
613 614 choices.extend([x[0] for x in bookmarks_group[0]])
614 615
615 616 tags_group = ([(k, k) for k, v in
616 617 repo.tags.iteritems()], _("Tags"))
617 618 hist_l.append(tags_group)
618 619 choices.extend([x[0] for x in tags_group[0]])
619 620
620 621 return choices, hist_l
621 622
622 623 def install_git_hook(self, repo, force_create=False):
623 624 """
624 625 Creates a rhodecode hook inside a git repository
625 626
626 627 :param repo: Instance of VCS repo
627 628 :param force_create: Create even if same name hook exists
628 629 """
629 630
630 631 loc = jn(repo.path, 'hooks')
631 632 if not repo.bare:
632 633 loc = jn(repo.path, '.git', 'hooks')
633 634 if not os.path.isdir(loc):
634 635 os.makedirs(loc)
635 636
636 637 tmpl_post = pkg_resources.resource_string(
637 638 'rhodecode', jn('config', 'post_receive_tmpl.py')
638 639 )
639 640 tmpl_pre = pkg_resources.resource_string(
640 641 'rhodecode', jn('config', 'pre_receive_tmpl.py')
641 642 )
642 643
643 644 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
644 645 _hook_file = jn(loc, '%s-receive' % h_type)
645 646 _rhodecode_hook = False
646 647 log.debug('Installing git hook in repo %s' % repo)
647 648 if os.path.exists(_hook_file):
648 649 # let's take a look at this hook, maybe it's rhodecode ?
649 650 log.debug('hook exists, checking if it is from rhodecode')
650 651 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
651 652 with open(_hook_file, 'rb') as f:
652 653 data = f.read()
653 654 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
654 655 % 'RC_HOOK_VER').search(data)
655 656 if matches:
656 657 try:
657 658 ver = matches.groups()[0]
658 659 log.debug('got %s it is rhodecode' % (ver))
659 660 _rhodecode_hook = True
660 661 except:
661 662 log.error(traceback.format_exc())
662 663 else:
663 664 # there is no hook in this dir, so we want to create one
664 665 _rhodecode_hook = True
665 666
666 667 if _rhodecode_hook or force_create:
667 668 log.debug('writing %s hook file !' % h_type)
668 669 with open(_hook_file, 'wb') as f:
669 670 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
670 671 f.write(tmpl)
671 672 os.chmod(_hook_file, 0755)
672 673 else:
673 674 log.debug('skipping writing hook file')
@@ -1,209 +1,209 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${h.link_to(_(u'Home'),h.url('/'))}
9 9 &raquo;
10 10 ${h.repo_link(c.rhodecode_db_repo.groups_and_repo)}
11 11 &raquo;
12 12 ${_('new pull request')}
13 13 </%def>
14 14
15 15 <%def name="main()">
16
16 ${self.context_bar('showpullrequest')}
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22 ${h.form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
23 23 <div style="float:left;padding:0px 30px 30px 30px">
24 24 ##ORG
25 25 <div style="float:left">
26 26 <div>
27 27 <span style="font-size: 20px">
28 28 ${h.select('org_repo','',c.org_repos,class_='refs')}:${h.select('org_ref',c.default_org_ref,c.org_refs,class_='refs')}
29 29 </span>
30 30 <div style="padding:5px 3px 3px 20px;">${c.rhodecode_db_repo.description}</div>
31 31 </div>
32 32 <div style="clear:both;padding-top: 10px"></div>
33 33 </div>
34 34 <div style="float:left;font-size:24px;padding:0px 20px">
35 35 <img height=32 width=32 src="${h.url('/images/arrow_right_64.png')}"/>
36 36 </div>
37 37
38 38 ##OTHER, most Probably the PARENT OF THIS FORK
39 39 <div style="float:left">
40 40 <div>
41 41 <span style="font-size: 20px">
42 42 ${h.select('other_repo',c.default_other_repo,c.other_repos,class_='refs')}:${h.select('other_ref',c.default_other_ref,c.default_other_refs,class_='refs')}
43 43 </span>
44 44 <div id="other_repo_desc" style="padding:5px 3px 3px 20px;"></div>
45 45 </div>
46 46 <div style="clear:both;padding-top: 10px"></div>
47 47 </div>
48 48 <div style="clear:both;padding-top: 10px"></div>
49 49 ## overview pulled by ajax
50 50 <div style="float:left" id="pull_request_overview"></div>
51 51 <div style="float:left;clear:both;padding:10px 10px 10px 0px;display:none">
52 52 <a id="pull_request_overview_url" href="#">${_('Detailed compare view')}</a>
53 53 </div>
54 54 </div>
55 55 <div style="float:left; border-left:1px dashed #eee">
56 56 <h4>${_('Pull request reviewers')}</h4>
57 57 <div id="reviewers" style="padding:0px 0px 0px 15px">
58 58 ## members goes here !
59 59 <div class="group_members_wrap">
60 60 <ul id="review_members" class="group_members">
61 61 %for member in c.review_members:
62 62 <li id="reviewer_${member.user_id}">
63 63 <div class="reviewers_member">
64 64 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
65 65 <div style="float:left">${member.full_name} (${_('owner')})</div>
66 66 <input type="hidden" value="${member.user_id}" name="review_members" />
67 67 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
68 68 </div>
69 69 </li>
70 70 %endfor
71 71 </ul>
72 72 </div>
73 73
74 74 <div class='ac'>
75 75 <div class="reviewer_ac">
76 76 ${h.text('user', class_='yui-ac-input')}
77 77 <span class="help-block">${_('Add reviewer to this pull request.')}</span>
78 78 <div id="reviewers_container"></div>
79 79 </div>
80 80 </div>
81 81 </div>
82 82 </div>
83 83 <h3>${_('Create new pull request')}</h3>
84 84
85 85 <div class="form">
86 86 <!-- fields -->
87 87
88 88 <div class="fields">
89 89
90 90 <div class="field">
91 91 <div class="label">
92 92 <label for="pullrequest_title">${_('Title')}:</label>
93 93 </div>
94 94 <div class="input">
95 95 ${h.text('pullrequest_title',size=30)}
96 96 </div>
97 97 </div>
98 98
99 99 <div class="field">
100 100 <div class="label label-textarea">
101 101 <label for="pullrequest_desc">${_('Description')}:</label>
102 102 </div>
103 103 <div class="textarea text-area editor">
104 104 ${h.textarea('pullrequest_desc',size=30)}
105 105 </div>
106 106 </div>
107 107
108 108 <div class="buttons">
109 109 ${h.submit('save',_('Send pull request'),class_="ui-btn large")}
110 110 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
111 111 </div>
112 112 </div>
113 113 </div>
114 114 ${h.end_form()}
115 115
116 116 </div>
117 117
118 118 <script type="text/javascript">
119 119 var _USERS_AC_DATA = ${c.users_array|n};
120 120 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
121 121 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
122 122
123 123 var other_repos_info = ${c.other_repos_info|n};
124 124
125 125 var otherrepoChanged = function(){
126 126 var sel_box = YUQ('#pull_request_form #other_repo')[0];
127 127 var repo_name = sel_box.options[sel_box.selectedIndex].value;
128 128
129 129 YUD.get('other_repo_desc').innerHTML = other_repos_info[repo_name]['description'];
130 130 // replace options of other_ref with the ones for the current other_repo
131 131 var other_ref_selector = YUD.get('other_ref');
132 132 var new_select = YUD.createElementFromMarkup(other_repos_info[repo_name]['revs']);
133 133 var new_selectedIndex = new_select.selectedIndex;
134 134 other_ref_selector.innerHTML = ""; // clear old options
135 135 while (new_select.length > 0){ // children will be popped when appened to other_ref_selector
136 136 other_ref_selector.appendChild(new_select.children[0]);
137 137 }
138 138 // browsers lost track of selected when appendChild was used
139 139 other_ref_selector.selectedIndex = new_selectedIndex;
140 140
141 141 // reset && add the reviewer based on selected repo
142 142 var _data = other_repos_info[repo_name];
143 143 YUD.get('review_members').innerHTML = '';
144 144 addReviewMember(_data.user.user_id, _data.user.firstname,
145 145 _data.user.lastname, _data.user.username,
146 146 _data.user.gravatar_link);
147 147 }
148 148
149 149 var loadPreview = function(){
150 150 //url template
151 151 var url = "${h.url('compare_url',
152 152 repo_name='__other_repo__',
153 153 org_ref_type='__other_ref_type__',
154 154 org_ref='__other_ref__',
155 155 other_repo='__org_repo__',
156 156 other_ref_type='__org_ref_type__',
157 157 other_ref='__org_ref__',
158 158 as_form=True,
159 159 merge=True,
160 160 )}";
161 161 var org_repo = YUQ('#pull_request_form #org_repo')[0].value;
162 162 var org_ref = YUQ('#pull_request_form #org_ref')[0].value.split(':');
163 163
164 164 var other_repo = YUQ('#pull_request_form #other_repo')[0].value;
165 165 var other_ref = YUQ('#pull_request_form #other_ref')[0].value.split(':');
166 166
167 167 var select_refs = YUQ('#pull_request_form select.refs')
168 168 var rev_data = {
169 169 'org_repo': org_repo,
170 170 'org_ref': org_ref[2],
171 171 'org_ref_type': 'rev',
172 172 'other_repo': other_repo,
173 173 'other_ref': other_ref[2],
174 174 'other_ref_type': 'rev',
175 175 }; // gather the org/other ref and repo here
176 176
177 177 for (k in rev_data){
178 178 url = url.replace('__'+k+'__',rev_data[k]);
179 179 }
180 180
181 181 YUD.get('pull_request_overview').innerHTML = "${_('Loading ...')}";
182 182 ypjax(url,'pull_request_overview');
183 183
184 184 YUD.get('pull_request_overview_url').href = url; // shouldn't have as_form ... but ...
185 185 YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','');
186 186 }
187 187
188 188 ## refresh automatically when something changes (org_repo can't change)
189 189
190 190 YUE.on('org_ref', 'change', function(e){
191 191 loadPreview();
192 192 });
193 193
194 194 YUE.on('other_repo', 'change', function(e){
195 195 otherrepoChanged();
196 196 loadPreview();
197 197 });
198 198
199 199 YUE.on('other_ref', 'change', function(e){
200 200 loadPreview();
201 201 });
202 202
203 203 otherrepoChanged();
204 204 //lazy load overview after 0.5s
205 205 setTimeout(loadPreview, 500);
206 206
207 207 </script>
208 208
209 209 </%def>
@@ -1,248 +1,248 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${h.link_to(_(u'Home'),h.url('/'))}
9 9 &raquo;
10 10 ${h.repo_link(c.rhodecode_db_repo.groups_and_repo)}
11 11 &raquo;
12 12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
13 13 </%def>
14 14
15 15 <%def name="main()">
16
16 ${self.context_bar('showpullrequest')}
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22 %if c.pull_request.is_closed():
23 23 <div style="padding:10px; font-size:22px;width:100%;text-align: center; color:#88D882">${_('Closed %s') % (h.age(c.pull_request.updated_on))} ${_('with status %s') % h.changeset_status_lbl(c.current_changeset_status)}</div>
24 24 %endif
25 25 <h3>
26 26 %if c.pull_request.is_closed():
27 27 <img src="${h.url('/images/icons/lock_go.png')}" title="${_('Closed')}"/>
28 28 %endif
29 29 <img src="${h.url('/images/icons/flag_status_%s.png' % str(c.pull_request.last_review_status))}" />
30 30 ${_('Title')}: ${c.pull_request.title}</h3>
31 31
32 32 <div class="form">
33 33 <div id="summary" class="fields">
34 34 <div class="field">
35 35 <div class="label-summary">
36 36 <label>${_('Review status')}:</label>
37 37 </div>
38 38 <div class="input">
39 39 <div class="changeset-status-container" style="float:none;clear:both">
40 40 %if c.current_changeset_status:
41 41 <div title="${_('Pull request status')}" class="changeset-status-lbl">${h.changeset_status_lbl(c.current_changeset_status)}</div>
42 42 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
43 43 %endif
44 44 </div>
45 45 </div>
46 46 </div>
47 47 <div class="field">
48 48 <div class="label-summary">
49 49 <label>${_('Still not reviewed by')}:</label>
50 50 </div>
51 51 <div class="input">
52 52 % if len(c.pull_request_pending_reviewers) > 0:
53 53 <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>
54 54 %else:
55 55 <div>${_('pull request was reviewed by all reviewers')}</div>
56 56 %endif
57 57 </div>
58 58 </div>
59 59 <div class="field">
60 60 <div class="label-summary">
61 61 <label>${_('Origin repository')}:</label>
62 62 </div>
63 63 <div class="input">
64 64 <div>
65 65 ##%if h.is_hg(c.pull_request.org_repo):
66 66 ## <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
67 67 ##%elif h.is_git(c.pull_request.org_repo):
68 68 ## <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
69 69 ##%endif
70 70 <span class="spantag">${c.pull_request.org_ref_parts[0]}: ${c.pull_request.org_ref_parts[1]}</span>
71 71 <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>
72 72 </div>
73 73 </div>
74 74 </div>
75 75 <div class="field">
76 76 <div class="label-summary">
77 77 <label>${_('Summary')}:</label>
78 78 </div>
79 79 <div class="input">
80 80 <div style="white-space:pre-wrap">${h.literal(c.pull_request.description)}</div>
81 81 </div>
82 82 </div>
83 83 <div class="field">
84 84 <div class="label-summary">
85 85 <label>${_('Created on')}:</label>
86 86 </div>
87 87 <div class="input">
88 88 <div>${h.fmt_date(c.pull_request.created_on)}</div>
89 89 </div>
90 90 </div>
91 91 </div>
92 92 </div>
93 93
94 94 <div style="overflow: auto;">
95 95 ##DIFF
96 96 <div class="table" style="float:left;clear:none">
97 97 <div id="body" class="diffblock">
98 98 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
99 99 </div>
100 100 <div id="changeset_compare_view_content">
101 101 ##CS
102 102 <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>
103 103 <%include file="/compare/compare_cs.html" />
104 104
105 105 ## FILES
106 106 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
107 107
108 108 % if c.limited_diff:
109 109 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
110 110 % else:
111 111 ${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)}:
112 112 %endif
113 113
114 114 </div>
115 115 <div class="cs_files">
116 116 %if not c.files:
117 117 <span class="empty_data">${_('No files')}</span>
118 118 %endif
119 119 %for fid, change, f, stat in c.files:
120 120 <div class="cs_${change}">
121 121 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
122 122 <div class="changes">${h.fancy_file_stats(stat)}</div>
123 123 </div>
124 124 %endfor
125 125 </div>
126 126 % if c.limited_diff:
127 127 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("confirm to show potentially huge diff")}')">${_('Show full diff')}</a></h5>
128 128 % endif
129 129 </div>
130 130 </div>
131 131 ## REVIEWERS
132 132 <div style="float:left; border-left:1px dashed #eee">
133 133 <h4>${_('Pull request reviewers')}</h4>
134 134 <div id="reviewers" style="padding:0px 0px 5px 10px">
135 135 ## members goes here !
136 136 <div class="group_members_wrap" style="min-height:45px">
137 137 <ul id="review_members" class="group_members">
138 138 %for member,status in c.pull_request_reviewers:
139 139 <li id="reviewer_${member.user_id}">
140 140 <div class="reviewers_member">
141 141 <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'))}">
142 142 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
143 143 </div>
144 144 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
145 145 <div style="float:left">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
146 146 <input type="hidden" value="${member.user_id}" name="review_members" />
147 147 %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):
148 148 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
149 149 %endif
150 150 </div>
151 151 </li>
152 152 %endfor
153 153 </ul>
154 154 </div>
155 155 %if not c.pull_request.is_closed():
156 156 <div class='ac'>
157 157 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
158 158 <div class="reviewer_ac">
159 159 ${h.text('user', class_='yui-ac-input')}
160 160 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
161 161 <div id="reviewers_container"></div>
162 162 </div>
163 163 <div style="padding:0px 10px">
164 164 <span id="update_pull_request" class="ui-btn xsmall">${_('Save changes')}</span>
165 165 </div>
166 166 %endif
167 167 </div>
168 168 %endif
169 169 </div>
170 170 </div>
171 171 </div>
172 172 <script>
173 173 var _USERS_AC_DATA = ${c.users_array|n};
174 174 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
175 175 // TODO: switch this to pyroutes
176 176 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
177 177 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
178 178
179 179 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
180 180 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
181 181 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
182 182
183 183 </script>
184 184
185 185 ## diff block
186 186 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
187 187 %for fid, change, f, stat in c.files:
188 188 ${diff_block.diff_block_simple([c.changes[fid]])}
189 189 %endfor
190 190 % if c.limited_diff:
191 191 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("confirm to show potentially huge diff")}')">${_('Show full diff')}</a></h4>
192 192 % endif
193 193
194 194
195 195 ## template for inline comment form
196 196 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
197 197 ${comment.comment_inline_form()}
198 198
199 199 ## render comments and inlines
200 200 ${comment.generate_comments(include_pr=True)}
201 201
202 202 % if not c.pull_request.is_closed():
203 203 ## main comment form and it status
204 204 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
205 205 pull_request_id=c.pull_request.pull_request_id),
206 206 c.current_changeset_status,
207 207 close_btn=True, change_status=c.allowed_to_change_status)}
208 208 %endif
209 209
210 210 <script type="text/javascript">
211 211 YUE.onDOMReady(function(){
212 212 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
213 213
214 214 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
215 215 var show = 'none';
216 216 var target = e.currentTarget;
217 217 if(target.checked){
218 218 var show = ''
219 219 }
220 220 var boxid = YUD.getAttribute(target,'id_for');
221 221 var comments = YUQ('#{0} .inline-comments'.format(boxid));
222 222 for(c in comments){
223 223 YUD.setStyle(comments[c],'display',show);
224 224 }
225 225 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
226 226 for(c in btns){
227 227 YUD.setStyle(btns[c],'display',show);
228 228 }
229 229 })
230 230
231 231 YUE.on(YUQ('.line'),'click',function(e){
232 232 var tr = e.currentTarget;
233 233 injectInlineForm(tr);
234 234 });
235 235
236 236 // inject comments into they proper positions
237 237 var file_comments = YUQ('.inline-comment-placeholder');
238 238 renderInlineComments(file_comments);
239 239
240 240 YUE.on(YUD.get('update_pull_request'),'click',function(e){
241 241 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
242 242 })
243 243 })
244 244 </script>
245 245
246 246 </div>
247 247
248 248 </%def>
@@ -1,40 +1,38 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('all pull requests')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('Pull requests')}
9 9 </%def>
10 10
11 11 <%def name="main()">
12 12 ${self.context_bar('showpullrequest')}
13 13 <div class="box">
14 14 <!-- box / title -->
15 15 <div class="title">
16 16 ${self.breadcrumbs()}
17 17 </div>
18 18
19 19 %for pr in c.pull_requests:
20 20 <div>
21 21 <h4 style="border:0px;padding:0px">
22 22 %if pr.is_closed():
23 23 <img src="${h.url('/images/icons/lock_go.png')}" title="${_('Closed')}"/>
24 24 %endif
25 25 <img src="${h.url('/images/icons/flag_status_%s.png' % str(pr.last_review_status))}" />
26 26 <a href="${h.url('pullrequest_show',repo_name=c.repo_name,pull_request_id=pr.pull_request_id)}">
27 27 ${_('Pull request #%s opened by %s on %s') % (pr.pull_request_id, pr.author.full_name, h.fmt_date(pr.created_on))}
28 28 </a>
29 29 </h4>
30 30 <h5 style="border:0px;padding-bottom:0px">${_('Title')}: ${pr.title}</h5>
31 31 <div style="padding:0px 24px">${pr.description}</div>
32 32 <div style="border-bottom: 1px solid #DDD;margin:10px 20px;padding-bottom:10px"></div>
33 33 </div>
34 34 %endfor
35 35
36 36 </div>
37 37
38 <script type="text/javascript"></script>
39
40 38 </%def>
General Comments 0
You need to be logged in to leave comments. Login now