##// END OF EJS Templates
db: use a wrapper on pull requests _last_merge_status to ensure this is always INT....
marcink -
r1968:ea1add97 default
parent child Browse files
Show More
@@ -1,4111 +1,4119 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from zope.cachedescriptors.property import Lazy as LazyProperty
45 from zope.cachedescriptors.property import Lazy as LazyProperty
46
46
47 from pyramid.threadlocal import get_current_request
47 from pyramid.threadlocal import get_current_request
48
48
49 from rhodecode.translation import _
49 from rhodecode.translation import _
50 from rhodecode.lib.vcs import get_vcs_instance
50 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.utils2 import (
52 from rhodecode.lib.utils2 import (
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 glob2re, StrictAttributeDict, cleaned_uri)
55 glob2re, StrictAttributeDict, cleaned_uri)
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.ext_json import json
57 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.caching_query import FromCache
58 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.encrypt import AESCipher
59 from rhodecode.lib.encrypt import AESCipher
60
60
61 from rhodecode.model.meta import Base, Session
61 from rhodecode.model.meta import Base, Session
62
62
63 URL_SEP = '/'
63 URL_SEP = '/'
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66 # =============================================================================
66 # =============================================================================
67 # BASE CLASSES
67 # BASE CLASSES
68 # =============================================================================
68 # =============================================================================
69
69
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # beaker.session.secret if first is not set.
71 # beaker.session.secret if first is not set.
72 # and initialized at environment.py
72 # and initialized at environment.py
73 ENCRYPTION_KEY = None
73 ENCRYPTION_KEY = None
74
74
75 # used to sort permissions by types, '#' used here is not allowed to be in
75 # used to sort permissions by types, '#' used here is not allowed to be in
76 # usernames, and it's very early in sorted string.printable table.
76 # usernames, and it's very early in sorted string.printable table.
77 PERMISSION_TYPE_SORT = {
77 PERMISSION_TYPE_SORT = {
78 'admin': '####',
78 'admin': '####',
79 'write': '###',
79 'write': '###',
80 'read': '##',
80 'read': '##',
81 'none': '#',
81 'none': '#',
82 }
82 }
83
83
84
84
85 def display_sort(obj):
85 def display_sort(obj):
86 """
86 """
87 Sort function used to sort permissions in .permissions() function of
87 Sort function used to sort permissions in .permissions() function of
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 of all other resources
89 of all other resources
90 """
90 """
91
91
92 if obj.username == User.DEFAULT_USER:
92 if obj.username == User.DEFAULT_USER:
93 return '#####'
93 return '#####'
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 return prefix + obj.username
95 return prefix + obj.username
96
96
97
97
98 def _hash_key(k):
98 def _hash_key(k):
99 return md5_safe(k)
99 return md5_safe(k)
100
100
101
101
102 class EncryptedTextValue(TypeDecorator):
102 class EncryptedTextValue(TypeDecorator):
103 """
103 """
104 Special column for encrypted long text data, use like::
104 Special column for encrypted long text data, use like::
105
105
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107
107
108 This column is intelligent so if value is in unencrypted form it return
108 This column is intelligent so if value is in unencrypted form it return
109 unencrypted form, but on save it always encrypts
109 unencrypted form, but on save it always encrypts
110 """
110 """
111 impl = Text
111 impl = Text
112
112
113 def process_bind_param(self, value, dialect):
113 def process_bind_param(self, value, dialect):
114 if not value:
114 if not value:
115 return value
115 return value
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 # protect against double encrypting if someone manually starts
117 # protect against double encrypting if someone manually starts
118 # doing
118 # doing
119 raise ValueError('value needs to be in unencrypted format, ie. '
119 raise ValueError('value needs to be in unencrypted format, ie. '
120 'not starting with enc$aes')
120 'not starting with enc$aes')
121 return 'enc$aes_hmac$%s' % AESCipher(
121 return 'enc$aes_hmac$%s' % AESCipher(
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
123
123
124 def process_result_value(self, value, dialect):
124 def process_result_value(self, value, dialect):
125 import rhodecode
125 import rhodecode
126
126
127 if not value:
127 if not value:
128 return value
128 return value
129
129
130 parts = value.split('$', 3)
130 parts = value.split('$', 3)
131 if not len(parts) == 3:
131 if not len(parts) == 3:
132 # probably not encrypted values
132 # probably not encrypted values
133 return value
133 return value
134 else:
134 else:
135 if parts[0] != 'enc':
135 if parts[0] != 'enc':
136 # parts ok but without our header ?
136 # parts ok but without our header ?
137 return value
137 return value
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 'rhodecode.encrypted_values.strict') or True)
139 'rhodecode.encrypted_values.strict') or True)
140 # at that stage we know it's our encryption
140 # at that stage we know it's our encryption
141 if parts[1] == 'aes':
141 if parts[1] == 'aes':
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 elif parts[1] == 'aes_hmac':
143 elif parts[1] == 'aes_hmac':
144 decrypted_data = AESCipher(
144 decrypted_data = AESCipher(
145 ENCRYPTION_KEY, hmac=True,
145 ENCRYPTION_KEY, hmac=True,
146 strict_verification=enc_strict_mode).decrypt(parts[2])
146 strict_verification=enc_strict_mode).decrypt(parts[2])
147 else:
147 else:
148 raise ValueError(
148 raise ValueError(
149 'Encryption type part is wrong, must be `aes` '
149 'Encryption type part is wrong, must be `aes` '
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 return decrypted_data
151 return decrypted_data
152
152
153
153
154 class BaseModel(object):
154 class BaseModel(object):
155 """
155 """
156 Base Model for all classes
156 Base Model for all classes
157 """
157 """
158
158
159 @classmethod
159 @classmethod
160 def _get_keys(cls):
160 def _get_keys(cls):
161 """return column names for this model """
161 """return column names for this model """
162 return class_mapper(cls).c.keys()
162 return class_mapper(cls).c.keys()
163
163
164 def get_dict(self):
164 def get_dict(self):
165 """
165 """
166 return dict with keys and values corresponding
166 return dict with keys and values corresponding
167 to this model data """
167 to this model data """
168
168
169 d = {}
169 d = {}
170 for k in self._get_keys():
170 for k in self._get_keys():
171 d[k] = getattr(self, k)
171 d[k] = getattr(self, k)
172
172
173 # also use __json__() if present to get additional fields
173 # also use __json__() if present to get additional fields
174 _json_attr = getattr(self, '__json__', None)
174 _json_attr = getattr(self, '__json__', None)
175 if _json_attr:
175 if _json_attr:
176 # update with attributes from __json__
176 # update with attributes from __json__
177 if callable(_json_attr):
177 if callable(_json_attr):
178 _json_attr = _json_attr()
178 _json_attr = _json_attr()
179 for k, val in _json_attr.iteritems():
179 for k, val in _json_attr.iteritems():
180 d[k] = val
180 d[k] = val
181 return d
181 return d
182
182
183 def get_appstruct(self):
183 def get_appstruct(self):
184 """return list with keys and values tuples corresponding
184 """return list with keys and values tuples corresponding
185 to this model data """
185 to this model data """
186
186
187 l = []
187 l = []
188 for k in self._get_keys():
188 for k in self._get_keys():
189 l.append((k, getattr(self, k),))
189 l.append((k, getattr(self, k),))
190 return l
190 return l
191
191
192 def populate_obj(self, populate_dict):
192 def populate_obj(self, populate_dict):
193 """populate model with data from given populate_dict"""
193 """populate model with data from given populate_dict"""
194
194
195 for k in self._get_keys():
195 for k in self._get_keys():
196 if k in populate_dict:
196 if k in populate_dict:
197 setattr(self, k, populate_dict[k])
197 setattr(self, k, populate_dict[k])
198
198
199 @classmethod
199 @classmethod
200 def query(cls):
200 def query(cls):
201 return Session().query(cls)
201 return Session().query(cls)
202
202
203 @classmethod
203 @classmethod
204 def get(cls, id_):
204 def get(cls, id_):
205 if id_:
205 if id_:
206 return cls.query().get(id_)
206 return cls.query().get(id_)
207
207
208 @classmethod
208 @classmethod
209 def get_or_404(cls, id_):
209 def get_or_404(cls, id_):
210 from pyramid.httpexceptions import HTTPNotFound
210 from pyramid.httpexceptions import HTTPNotFound
211
211
212 try:
212 try:
213 id_ = int(id_)
213 id_ = int(id_)
214 except (TypeError, ValueError):
214 except (TypeError, ValueError):
215 raise HTTPNotFound()
215 raise HTTPNotFound()
216
216
217 res = cls.query().get(id_)
217 res = cls.query().get(id_)
218 if not res:
218 if not res:
219 raise HTTPNotFound()
219 raise HTTPNotFound()
220 return res
220 return res
221
221
222 @classmethod
222 @classmethod
223 def getAll(cls):
223 def getAll(cls):
224 # deprecated and left for backward compatibility
224 # deprecated and left for backward compatibility
225 return cls.get_all()
225 return cls.get_all()
226
226
227 @classmethod
227 @classmethod
228 def get_all(cls):
228 def get_all(cls):
229 return cls.query().all()
229 return cls.query().all()
230
230
231 @classmethod
231 @classmethod
232 def delete(cls, id_):
232 def delete(cls, id_):
233 obj = cls.query().get(id_)
233 obj = cls.query().get(id_)
234 Session().delete(obj)
234 Session().delete(obj)
235
235
236 @classmethod
236 @classmethod
237 def identity_cache(cls, session, attr_name, value):
237 def identity_cache(cls, session, attr_name, value):
238 exist_in_session = []
238 exist_in_session = []
239 for (item_cls, pkey), instance in session.identity_map.items():
239 for (item_cls, pkey), instance in session.identity_map.items():
240 if cls == item_cls and getattr(instance, attr_name) == value:
240 if cls == item_cls and getattr(instance, attr_name) == value:
241 exist_in_session.append(instance)
241 exist_in_session.append(instance)
242 if exist_in_session:
242 if exist_in_session:
243 if len(exist_in_session) == 1:
243 if len(exist_in_session) == 1:
244 return exist_in_session[0]
244 return exist_in_session[0]
245 log.exception(
245 log.exception(
246 'multiple objects with attr %s and '
246 'multiple objects with attr %s and '
247 'value %s found with same name: %r',
247 'value %s found with same name: %r',
248 attr_name, value, exist_in_session)
248 attr_name, value, exist_in_session)
249
249
250 def __repr__(self):
250 def __repr__(self):
251 if hasattr(self, '__unicode__'):
251 if hasattr(self, '__unicode__'):
252 # python repr needs to return str
252 # python repr needs to return str
253 try:
253 try:
254 return safe_str(self.__unicode__())
254 return safe_str(self.__unicode__())
255 except UnicodeDecodeError:
255 except UnicodeDecodeError:
256 pass
256 pass
257 return '<DB:%s>' % (self.__class__.__name__)
257 return '<DB:%s>' % (self.__class__.__name__)
258
258
259
259
260 class RhodeCodeSetting(Base, BaseModel):
260 class RhodeCodeSetting(Base, BaseModel):
261 __tablename__ = 'rhodecode_settings'
261 __tablename__ = 'rhodecode_settings'
262 __table_args__ = (
262 __table_args__ = (
263 UniqueConstraint('app_settings_name'),
263 UniqueConstraint('app_settings_name'),
264 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 {'extend_existing': True, 'mysql_engine': 'InnoDB',
265 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
266 )
266 )
267
267
268 SETTINGS_TYPES = {
268 SETTINGS_TYPES = {
269 'str': safe_str,
269 'str': safe_str,
270 'int': safe_int,
270 'int': safe_int,
271 'unicode': safe_unicode,
271 'unicode': safe_unicode,
272 'bool': str2bool,
272 'bool': str2bool,
273 'list': functools.partial(aslist, sep=',')
273 'list': functools.partial(aslist, sep=',')
274 }
274 }
275 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
276 GLOBAL_CONF_KEY = 'app_settings'
276 GLOBAL_CONF_KEY = 'app_settings'
277
277
278 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
279 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
280 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
281 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
282
282
283 def __init__(self, key='', val='', type='unicode'):
283 def __init__(self, key='', val='', type='unicode'):
284 self.app_settings_name = key
284 self.app_settings_name = key
285 self.app_settings_type = type
285 self.app_settings_type = type
286 self.app_settings_value = val
286 self.app_settings_value = val
287
287
288 @validates('_app_settings_value')
288 @validates('_app_settings_value')
289 def validate_settings_value(self, key, val):
289 def validate_settings_value(self, key, val):
290 assert type(val) == unicode
290 assert type(val) == unicode
291 return val
291 return val
292
292
293 @hybrid_property
293 @hybrid_property
294 def app_settings_value(self):
294 def app_settings_value(self):
295 v = self._app_settings_value
295 v = self._app_settings_value
296 _type = self.app_settings_type
296 _type = self.app_settings_type
297 if _type:
297 if _type:
298 _type = self.app_settings_type.split('.')[0]
298 _type = self.app_settings_type.split('.')[0]
299 # decode the encrypted value
299 # decode the encrypted value
300 if 'encrypted' in self.app_settings_type:
300 if 'encrypted' in self.app_settings_type:
301 cipher = EncryptedTextValue()
301 cipher = EncryptedTextValue()
302 v = safe_unicode(cipher.process_result_value(v, None))
302 v = safe_unicode(cipher.process_result_value(v, None))
303
303
304 converter = self.SETTINGS_TYPES.get(_type) or \
304 converter = self.SETTINGS_TYPES.get(_type) or \
305 self.SETTINGS_TYPES['unicode']
305 self.SETTINGS_TYPES['unicode']
306 return converter(v)
306 return converter(v)
307
307
308 @app_settings_value.setter
308 @app_settings_value.setter
309 def app_settings_value(self, val):
309 def app_settings_value(self, val):
310 """
310 """
311 Setter that will always make sure we use unicode in app_settings_value
311 Setter that will always make sure we use unicode in app_settings_value
312
312
313 :param val:
313 :param val:
314 """
314 """
315 val = safe_unicode(val)
315 val = safe_unicode(val)
316 # encode the encrypted value
316 # encode the encrypted value
317 if 'encrypted' in self.app_settings_type:
317 if 'encrypted' in self.app_settings_type:
318 cipher = EncryptedTextValue()
318 cipher = EncryptedTextValue()
319 val = safe_unicode(cipher.process_bind_param(val, None))
319 val = safe_unicode(cipher.process_bind_param(val, None))
320 self._app_settings_value = val
320 self._app_settings_value = val
321
321
322 @hybrid_property
322 @hybrid_property
323 def app_settings_type(self):
323 def app_settings_type(self):
324 return self._app_settings_type
324 return self._app_settings_type
325
325
326 @app_settings_type.setter
326 @app_settings_type.setter
327 def app_settings_type(self, val):
327 def app_settings_type(self, val):
328 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 if val.split('.')[0] not in self.SETTINGS_TYPES:
329 raise Exception('type must be one of %s got %s'
329 raise Exception('type must be one of %s got %s'
330 % (self.SETTINGS_TYPES.keys(), val))
330 % (self.SETTINGS_TYPES.keys(), val))
331 self._app_settings_type = val
331 self._app_settings_type = val
332
332
333 def __unicode__(self):
333 def __unicode__(self):
334 return u"<%s('%s:%s[%s]')>" % (
334 return u"<%s('%s:%s[%s]')>" % (
335 self.__class__.__name__,
335 self.__class__.__name__,
336 self.app_settings_name, self.app_settings_value,
336 self.app_settings_name, self.app_settings_value,
337 self.app_settings_type
337 self.app_settings_type
338 )
338 )
339
339
340
340
341 class RhodeCodeUi(Base, BaseModel):
341 class RhodeCodeUi(Base, BaseModel):
342 __tablename__ = 'rhodecode_ui'
342 __tablename__ = 'rhodecode_ui'
343 __table_args__ = (
343 __table_args__ = (
344 UniqueConstraint('ui_key'),
344 UniqueConstraint('ui_key'),
345 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 {'extend_existing': True, 'mysql_engine': 'InnoDB',
346 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
347 )
347 )
348
348
349 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 HOOK_REPO_SIZE = 'changegroup.repo_size'
350 # HG
350 # HG
351 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
352 HOOK_PULL = 'outgoing.pull_logger'
352 HOOK_PULL = 'outgoing.pull_logger'
353 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
354 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
354 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
355 HOOK_PUSH = 'changegroup.push_logger'
355 HOOK_PUSH = 'changegroup.push_logger'
356 HOOK_PUSH_KEY = 'pushkey.key_push'
356 HOOK_PUSH_KEY = 'pushkey.key_push'
357
357
358 # TODO: johbo: Unify way how hooks are configured for git and hg,
358 # TODO: johbo: Unify way how hooks are configured for git and hg,
359 # git part is currently hardcoded.
359 # git part is currently hardcoded.
360
360
361 # SVN PATTERNS
361 # SVN PATTERNS
362 SVN_BRANCH_ID = 'vcs_svn_branch'
362 SVN_BRANCH_ID = 'vcs_svn_branch'
363 SVN_TAG_ID = 'vcs_svn_tag'
363 SVN_TAG_ID = 'vcs_svn_tag'
364
364
365 ui_id = Column(
365 ui_id = Column(
366 "ui_id", Integer(), nullable=False, unique=True, default=None,
366 "ui_id", Integer(), nullable=False, unique=True, default=None,
367 primary_key=True)
367 primary_key=True)
368 ui_section = Column(
368 ui_section = Column(
369 "ui_section", String(255), nullable=True, unique=None, default=None)
369 "ui_section", String(255), nullable=True, unique=None, default=None)
370 ui_key = Column(
370 ui_key = Column(
371 "ui_key", String(255), nullable=True, unique=None, default=None)
371 "ui_key", String(255), nullable=True, unique=None, default=None)
372 ui_value = Column(
372 ui_value = Column(
373 "ui_value", String(255), nullable=True, unique=None, default=None)
373 "ui_value", String(255), nullable=True, unique=None, default=None)
374 ui_active = Column(
374 ui_active = Column(
375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
376
376
377 def __repr__(self):
377 def __repr__(self):
378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
379 self.ui_key, self.ui_value)
379 self.ui_key, self.ui_value)
380
380
381
381
382 class RepoRhodeCodeSetting(Base, BaseModel):
382 class RepoRhodeCodeSetting(Base, BaseModel):
383 __tablename__ = 'repo_rhodecode_settings'
383 __tablename__ = 'repo_rhodecode_settings'
384 __table_args__ = (
384 __table_args__ = (
385 UniqueConstraint(
385 UniqueConstraint(
386 'app_settings_name', 'repository_id',
386 'app_settings_name', 'repository_id',
387 name='uq_repo_rhodecode_setting_name_repo_id'),
387 name='uq_repo_rhodecode_setting_name_repo_id'),
388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
390 )
390 )
391
391
392 repository_id = Column(
392 repository_id = Column(
393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
394 nullable=False)
394 nullable=False)
395 app_settings_id = Column(
395 app_settings_id = Column(
396 "app_settings_id", Integer(), nullable=False, unique=True,
396 "app_settings_id", Integer(), nullable=False, unique=True,
397 default=None, primary_key=True)
397 default=None, primary_key=True)
398 app_settings_name = Column(
398 app_settings_name = Column(
399 "app_settings_name", String(255), nullable=True, unique=None,
399 "app_settings_name", String(255), nullable=True, unique=None,
400 default=None)
400 default=None)
401 _app_settings_value = Column(
401 _app_settings_value = Column(
402 "app_settings_value", String(4096), nullable=True, unique=None,
402 "app_settings_value", String(4096), nullable=True, unique=None,
403 default=None)
403 default=None)
404 _app_settings_type = Column(
404 _app_settings_type = Column(
405 "app_settings_type", String(255), nullable=True, unique=None,
405 "app_settings_type", String(255), nullable=True, unique=None,
406 default=None)
406 default=None)
407
407
408 repository = relationship('Repository')
408 repository = relationship('Repository')
409
409
410 def __init__(self, repository_id, key='', val='', type='unicode'):
410 def __init__(self, repository_id, key='', val='', type='unicode'):
411 self.repository_id = repository_id
411 self.repository_id = repository_id
412 self.app_settings_name = key
412 self.app_settings_name = key
413 self.app_settings_type = type
413 self.app_settings_type = type
414 self.app_settings_value = val
414 self.app_settings_value = val
415
415
416 @validates('_app_settings_value')
416 @validates('_app_settings_value')
417 def validate_settings_value(self, key, val):
417 def validate_settings_value(self, key, val):
418 assert type(val) == unicode
418 assert type(val) == unicode
419 return val
419 return val
420
420
421 @hybrid_property
421 @hybrid_property
422 def app_settings_value(self):
422 def app_settings_value(self):
423 v = self._app_settings_value
423 v = self._app_settings_value
424 type_ = self.app_settings_type
424 type_ = self.app_settings_type
425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
427 return converter(v)
427 return converter(v)
428
428
429 @app_settings_value.setter
429 @app_settings_value.setter
430 def app_settings_value(self, val):
430 def app_settings_value(self, val):
431 """
431 """
432 Setter that will always make sure we use unicode in app_settings_value
432 Setter that will always make sure we use unicode in app_settings_value
433
433
434 :param val:
434 :param val:
435 """
435 """
436 self._app_settings_value = safe_unicode(val)
436 self._app_settings_value = safe_unicode(val)
437
437
438 @hybrid_property
438 @hybrid_property
439 def app_settings_type(self):
439 def app_settings_type(self):
440 return self._app_settings_type
440 return self._app_settings_type
441
441
442 @app_settings_type.setter
442 @app_settings_type.setter
443 def app_settings_type(self, val):
443 def app_settings_type(self, val):
444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
445 if val not in SETTINGS_TYPES:
445 if val not in SETTINGS_TYPES:
446 raise Exception('type must be one of %s got %s'
446 raise Exception('type must be one of %s got %s'
447 % (SETTINGS_TYPES.keys(), val))
447 % (SETTINGS_TYPES.keys(), val))
448 self._app_settings_type = val
448 self._app_settings_type = val
449
449
450 def __unicode__(self):
450 def __unicode__(self):
451 return u"<%s('%s:%s:%s[%s]')>" % (
451 return u"<%s('%s:%s:%s[%s]')>" % (
452 self.__class__.__name__, self.repository.repo_name,
452 self.__class__.__name__, self.repository.repo_name,
453 self.app_settings_name, self.app_settings_value,
453 self.app_settings_name, self.app_settings_value,
454 self.app_settings_type
454 self.app_settings_type
455 )
455 )
456
456
457
457
458 class RepoRhodeCodeUi(Base, BaseModel):
458 class RepoRhodeCodeUi(Base, BaseModel):
459 __tablename__ = 'repo_rhodecode_ui'
459 __tablename__ = 'repo_rhodecode_ui'
460 __table_args__ = (
460 __table_args__ = (
461 UniqueConstraint(
461 UniqueConstraint(
462 'repository_id', 'ui_section', 'ui_key',
462 'repository_id', 'ui_section', 'ui_key',
463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
466 )
466 )
467
467
468 repository_id = Column(
468 repository_id = Column(
469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
470 nullable=False)
470 nullable=False)
471 ui_id = Column(
471 ui_id = Column(
472 "ui_id", Integer(), nullable=False, unique=True, default=None,
472 "ui_id", Integer(), nullable=False, unique=True, default=None,
473 primary_key=True)
473 primary_key=True)
474 ui_section = Column(
474 ui_section = Column(
475 "ui_section", String(255), nullable=True, unique=None, default=None)
475 "ui_section", String(255), nullable=True, unique=None, default=None)
476 ui_key = Column(
476 ui_key = Column(
477 "ui_key", String(255), nullable=True, unique=None, default=None)
477 "ui_key", String(255), nullable=True, unique=None, default=None)
478 ui_value = Column(
478 ui_value = Column(
479 "ui_value", String(255), nullable=True, unique=None, default=None)
479 "ui_value", String(255), nullable=True, unique=None, default=None)
480 ui_active = Column(
480 ui_active = Column(
481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
482
482
483 repository = relationship('Repository')
483 repository = relationship('Repository')
484
484
485 def __repr__(self):
485 def __repr__(self):
486 return '<%s[%s:%s]%s=>%s]>' % (
486 return '<%s[%s:%s]%s=>%s]>' % (
487 self.__class__.__name__, self.repository.repo_name,
487 self.__class__.__name__, self.repository.repo_name,
488 self.ui_section, self.ui_key, self.ui_value)
488 self.ui_section, self.ui_key, self.ui_value)
489
489
490
490
491 class User(Base, BaseModel):
491 class User(Base, BaseModel):
492 __tablename__ = 'users'
492 __tablename__ = 'users'
493 __table_args__ = (
493 __table_args__ = (
494 UniqueConstraint('username'), UniqueConstraint('email'),
494 UniqueConstraint('username'), UniqueConstraint('email'),
495 Index('u_username_idx', 'username'),
495 Index('u_username_idx', 'username'),
496 Index('u_email_idx', 'email'),
496 Index('u_email_idx', 'email'),
497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
499 )
499 )
500 DEFAULT_USER = 'default'
500 DEFAULT_USER = 'default'
501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
503
503
504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
505 username = Column("username", String(255), nullable=True, unique=None, default=None)
505 username = Column("username", String(255), nullable=True, unique=None, default=None)
506 password = Column("password", String(255), nullable=True, unique=None, default=None)
506 password = Column("password", String(255), nullable=True, unique=None, default=None)
507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
513 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
513 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
514
514
515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
517 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
517 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
521
521
522 user_log = relationship('UserLog')
522 user_log = relationship('UserLog')
523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
524
524
525 repositories = relationship('Repository')
525 repositories = relationship('Repository')
526 repository_groups = relationship('RepoGroup')
526 repository_groups = relationship('RepoGroup')
527 user_groups = relationship('UserGroup')
527 user_groups = relationship('UserGroup')
528
528
529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
531
531
532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
535
535
536 group_member = relationship('UserGroupMember', cascade='all')
536 group_member = relationship('UserGroupMember', cascade='all')
537
537
538 notifications = relationship('UserNotification', cascade='all')
538 notifications = relationship('UserNotification', cascade='all')
539 # notifications assigned to this user
539 # notifications assigned to this user
540 user_created_notifications = relationship('Notification', cascade='all')
540 user_created_notifications = relationship('Notification', cascade='all')
541 # comments created by this user
541 # comments created by this user
542 user_comments = relationship('ChangesetComment', cascade='all')
542 user_comments = relationship('ChangesetComment', cascade='all')
543 # user profile extra info
543 # user profile extra info
544 user_emails = relationship('UserEmailMap', cascade='all')
544 user_emails = relationship('UserEmailMap', cascade='all')
545 user_ip_map = relationship('UserIpMap', cascade='all')
545 user_ip_map = relationship('UserIpMap', cascade='all')
546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
547 # gists
547 # gists
548 user_gists = relationship('Gist', cascade='all')
548 user_gists = relationship('Gist', cascade='all')
549 # user pull requests
549 # user pull requests
550 user_pull_requests = relationship('PullRequest', cascade='all')
550 user_pull_requests = relationship('PullRequest', cascade='all')
551 # external identities
551 # external identities
552 extenal_identities = relationship(
552 extenal_identities = relationship(
553 'ExternalIdentity',
553 'ExternalIdentity',
554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
555 cascade='all')
555 cascade='all')
556
556
557 def __unicode__(self):
557 def __unicode__(self):
558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
559 self.user_id, self.username)
559 self.user_id, self.username)
560
560
561 @hybrid_property
561 @hybrid_property
562 def email(self):
562 def email(self):
563 return self._email
563 return self._email
564
564
565 @email.setter
565 @email.setter
566 def email(self, val):
566 def email(self, val):
567 self._email = val.lower() if val else None
567 self._email = val.lower() if val else None
568
568
569 @hybrid_property
569 @hybrid_property
570 def first_name(self):
570 def first_name(self):
571 from rhodecode.lib import helpers as h
571 from rhodecode.lib import helpers as h
572 if self.name:
572 if self.name:
573 return h.escape(self.name)
573 return h.escape(self.name)
574 return self.name
574 return self.name
575
575
576 @hybrid_property
576 @hybrid_property
577 def last_name(self):
577 def last_name(self):
578 from rhodecode.lib import helpers as h
578 from rhodecode.lib import helpers as h
579 if self.lastname:
579 if self.lastname:
580 return h.escape(self.lastname)
580 return h.escape(self.lastname)
581 return self.lastname
581 return self.lastname
582
582
583 @hybrid_property
583 @hybrid_property
584 def api_key(self):
584 def api_key(self):
585 """
585 """
586 Fetch if exist an auth-token with role ALL connected to this user
586 Fetch if exist an auth-token with role ALL connected to this user
587 """
587 """
588 user_auth_token = UserApiKeys.query()\
588 user_auth_token = UserApiKeys.query()\
589 .filter(UserApiKeys.user_id == self.user_id)\
589 .filter(UserApiKeys.user_id == self.user_id)\
590 .filter(or_(UserApiKeys.expires == -1,
590 .filter(or_(UserApiKeys.expires == -1,
591 UserApiKeys.expires >= time.time()))\
591 UserApiKeys.expires >= time.time()))\
592 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
592 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
593 if user_auth_token:
593 if user_auth_token:
594 user_auth_token = user_auth_token.api_key
594 user_auth_token = user_auth_token.api_key
595
595
596 return user_auth_token
596 return user_auth_token
597
597
598 @api_key.setter
598 @api_key.setter
599 def api_key(self, val):
599 def api_key(self, val):
600 # don't allow to set API key this is deprecated for now
600 # don't allow to set API key this is deprecated for now
601 self._api_key = None
601 self._api_key = None
602
602
603 @property
603 @property
604 def reviewer_pull_requests(self):
604 def reviewer_pull_requests(self):
605 return PullRequestReviewers.query() \
605 return PullRequestReviewers.query() \
606 .options(joinedload(PullRequestReviewers.pull_request)) \
606 .options(joinedload(PullRequestReviewers.pull_request)) \
607 .filter(PullRequestReviewers.user_id == self.user_id) \
607 .filter(PullRequestReviewers.user_id == self.user_id) \
608 .all()
608 .all()
609
609
610 @property
610 @property
611 def firstname(self):
611 def firstname(self):
612 # alias for future
612 # alias for future
613 return self.name
613 return self.name
614
614
615 @property
615 @property
616 def emails(self):
616 def emails(self):
617 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
617 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
618 return [self.email] + [x.email for x in other]
618 return [self.email] + [x.email for x in other]
619
619
620 @property
620 @property
621 def auth_tokens(self):
621 def auth_tokens(self):
622 auth_tokens = self.get_auth_tokens()
622 auth_tokens = self.get_auth_tokens()
623 return [x.api_key for x in auth_tokens]
623 return [x.api_key for x in auth_tokens]
624
624
625 def get_auth_tokens(self):
625 def get_auth_tokens(self):
626 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
626 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
627
627
628 @property
628 @property
629 def feed_token(self):
629 def feed_token(self):
630 return self.get_feed_token()
630 return self.get_feed_token()
631
631
632 def get_feed_token(self):
632 def get_feed_token(self):
633 feed_tokens = UserApiKeys.query()\
633 feed_tokens = UserApiKeys.query()\
634 .filter(UserApiKeys.user == self)\
634 .filter(UserApiKeys.user == self)\
635 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
635 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
636 .all()
636 .all()
637 if feed_tokens:
637 if feed_tokens:
638 return feed_tokens[0].api_key
638 return feed_tokens[0].api_key
639 return 'NO_FEED_TOKEN_AVAILABLE'
639 return 'NO_FEED_TOKEN_AVAILABLE'
640
640
641 @classmethod
641 @classmethod
642 def extra_valid_auth_tokens(cls, user, role=None):
642 def extra_valid_auth_tokens(cls, user, role=None):
643 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
643 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
644 .filter(or_(UserApiKeys.expires == -1,
644 .filter(or_(UserApiKeys.expires == -1,
645 UserApiKeys.expires >= time.time()))
645 UserApiKeys.expires >= time.time()))
646 if role:
646 if role:
647 tokens = tokens.filter(or_(UserApiKeys.role == role,
647 tokens = tokens.filter(or_(UserApiKeys.role == role,
648 UserApiKeys.role == UserApiKeys.ROLE_ALL))
648 UserApiKeys.role == UserApiKeys.ROLE_ALL))
649 return tokens.all()
649 return tokens.all()
650
650
651 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
651 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
652 from rhodecode.lib import auth
652 from rhodecode.lib import auth
653
653
654 log.debug('Trying to authenticate user: %s via auth-token, '
654 log.debug('Trying to authenticate user: %s via auth-token, '
655 'and roles: %s', self, roles)
655 'and roles: %s', self, roles)
656
656
657 if not auth_token:
657 if not auth_token:
658 return False
658 return False
659
659
660 crypto_backend = auth.crypto_backend()
660 crypto_backend = auth.crypto_backend()
661
661
662 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
662 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
663 tokens_q = UserApiKeys.query()\
663 tokens_q = UserApiKeys.query()\
664 .filter(UserApiKeys.user_id == self.user_id)\
664 .filter(UserApiKeys.user_id == self.user_id)\
665 .filter(or_(UserApiKeys.expires == -1,
665 .filter(or_(UserApiKeys.expires == -1,
666 UserApiKeys.expires >= time.time()))
666 UserApiKeys.expires >= time.time()))
667
667
668 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
668 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
669
669
670 plain_tokens = []
670 plain_tokens = []
671 hash_tokens = []
671 hash_tokens = []
672
672
673 for token in tokens_q.all():
673 for token in tokens_q.all():
674 # verify scope first
674 # verify scope first
675 if token.repo_id:
675 if token.repo_id:
676 # token has a scope, we need to verify it
676 # token has a scope, we need to verify it
677 if scope_repo_id != token.repo_id:
677 if scope_repo_id != token.repo_id:
678 log.debug(
678 log.debug(
679 'Scope mismatch: token has a set repo scope: %s, '
679 'Scope mismatch: token has a set repo scope: %s, '
680 'and calling scope is:%s, skipping further checks',
680 'and calling scope is:%s, skipping further checks',
681 token.repo, scope_repo_id)
681 token.repo, scope_repo_id)
682 # token has a scope, and it doesn't match, skip token
682 # token has a scope, and it doesn't match, skip token
683 continue
683 continue
684
684
685 if token.api_key.startswith(crypto_backend.ENC_PREF):
685 if token.api_key.startswith(crypto_backend.ENC_PREF):
686 hash_tokens.append(token.api_key)
686 hash_tokens.append(token.api_key)
687 else:
687 else:
688 plain_tokens.append(token.api_key)
688 plain_tokens.append(token.api_key)
689
689
690 is_plain_match = auth_token in plain_tokens
690 is_plain_match = auth_token in plain_tokens
691 if is_plain_match:
691 if is_plain_match:
692 return True
692 return True
693
693
694 for hashed in hash_tokens:
694 for hashed in hash_tokens:
695 # TODO(marcink): this is expensive to calculate, but most secure
695 # TODO(marcink): this is expensive to calculate, but most secure
696 match = crypto_backend.hash_check(auth_token, hashed)
696 match = crypto_backend.hash_check(auth_token, hashed)
697 if match:
697 if match:
698 return True
698 return True
699
699
700 return False
700 return False
701
701
702 @property
702 @property
703 def ip_addresses(self):
703 def ip_addresses(self):
704 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
704 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
705 return [x.ip_addr for x in ret]
705 return [x.ip_addr for x in ret]
706
706
707 @property
707 @property
708 def username_and_name(self):
708 def username_and_name(self):
709 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
709 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
710
710
711 @property
711 @property
712 def username_or_name_or_email(self):
712 def username_or_name_or_email(self):
713 full_name = self.full_name if self.full_name is not ' ' else None
713 full_name = self.full_name if self.full_name is not ' ' else None
714 return self.username or full_name or self.email
714 return self.username or full_name or self.email
715
715
716 @property
716 @property
717 def full_name(self):
717 def full_name(self):
718 return '%s %s' % (self.first_name, self.last_name)
718 return '%s %s' % (self.first_name, self.last_name)
719
719
720 @property
720 @property
721 def full_name_or_username(self):
721 def full_name_or_username(self):
722 return ('%s %s' % (self.first_name, self.last_name)
722 return ('%s %s' % (self.first_name, self.last_name)
723 if (self.first_name and self.last_name) else self.username)
723 if (self.first_name and self.last_name) else self.username)
724
724
725 @property
725 @property
726 def full_contact(self):
726 def full_contact(self):
727 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
727 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
728
728
729 @property
729 @property
730 def short_contact(self):
730 def short_contact(self):
731 return '%s %s' % (self.first_name, self.last_name)
731 return '%s %s' % (self.first_name, self.last_name)
732
732
733 @property
733 @property
734 def is_admin(self):
734 def is_admin(self):
735 return self.admin
735 return self.admin
736
736
737 @property
737 @property
738 def AuthUser(self):
738 def AuthUser(self):
739 """
739 """
740 Returns instance of AuthUser for this user
740 Returns instance of AuthUser for this user
741 """
741 """
742 from rhodecode.lib.auth import AuthUser
742 from rhodecode.lib.auth import AuthUser
743 return AuthUser(user_id=self.user_id, username=self.username)
743 return AuthUser(user_id=self.user_id, username=self.username)
744
744
745 @hybrid_property
745 @hybrid_property
746 def user_data(self):
746 def user_data(self):
747 if not self._user_data:
747 if not self._user_data:
748 return {}
748 return {}
749
749
750 try:
750 try:
751 return json.loads(self._user_data)
751 return json.loads(self._user_data)
752 except TypeError:
752 except TypeError:
753 return {}
753 return {}
754
754
755 @user_data.setter
755 @user_data.setter
756 def user_data(self, val):
756 def user_data(self, val):
757 if not isinstance(val, dict):
757 if not isinstance(val, dict):
758 raise Exception('user_data must be dict, got %s' % type(val))
758 raise Exception('user_data must be dict, got %s' % type(val))
759 try:
759 try:
760 self._user_data = json.dumps(val)
760 self._user_data = json.dumps(val)
761 except Exception:
761 except Exception:
762 log.error(traceback.format_exc())
762 log.error(traceback.format_exc())
763
763
764 @classmethod
764 @classmethod
765 def get_by_username(cls, username, case_insensitive=False,
765 def get_by_username(cls, username, case_insensitive=False,
766 cache=False, identity_cache=False):
766 cache=False, identity_cache=False):
767 session = Session()
767 session = Session()
768
768
769 if case_insensitive:
769 if case_insensitive:
770 q = cls.query().filter(
770 q = cls.query().filter(
771 func.lower(cls.username) == func.lower(username))
771 func.lower(cls.username) == func.lower(username))
772 else:
772 else:
773 q = cls.query().filter(cls.username == username)
773 q = cls.query().filter(cls.username == username)
774
774
775 if cache:
775 if cache:
776 if identity_cache:
776 if identity_cache:
777 val = cls.identity_cache(session, 'username', username)
777 val = cls.identity_cache(session, 'username', username)
778 if val:
778 if val:
779 return val
779 return val
780 else:
780 else:
781 cache_key = "get_user_by_name_%s" % _hash_key(username)
781 cache_key = "get_user_by_name_%s" % _hash_key(username)
782 q = q.options(
782 q = q.options(
783 FromCache("sql_cache_short", cache_key))
783 FromCache("sql_cache_short", cache_key))
784
784
785 return q.scalar()
785 return q.scalar()
786
786
787 @classmethod
787 @classmethod
788 def get_by_auth_token(cls, auth_token, cache=False):
788 def get_by_auth_token(cls, auth_token, cache=False):
789 q = UserApiKeys.query()\
789 q = UserApiKeys.query()\
790 .filter(UserApiKeys.api_key == auth_token)\
790 .filter(UserApiKeys.api_key == auth_token)\
791 .filter(or_(UserApiKeys.expires == -1,
791 .filter(or_(UserApiKeys.expires == -1,
792 UserApiKeys.expires >= time.time()))
792 UserApiKeys.expires >= time.time()))
793 if cache:
793 if cache:
794 q = q.options(
794 q = q.options(
795 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
795 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
796
796
797 match = q.first()
797 match = q.first()
798 if match:
798 if match:
799 return match.user
799 return match.user
800
800
801 @classmethod
801 @classmethod
802 def get_by_email(cls, email, case_insensitive=False, cache=False):
802 def get_by_email(cls, email, case_insensitive=False, cache=False):
803
803
804 if case_insensitive:
804 if case_insensitive:
805 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
805 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
806
806
807 else:
807 else:
808 q = cls.query().filter(cls.email == email)
808 q = cls.query().filter(cls.email == email)
809
809
810 email_key = _hash_key(email)
810 email_key = _hash_key(email)
811 if cache:
811 if cache:
812 q = q.options(
812 q = q.options(
813 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
813 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
814
814
815 ret = q.scalar()
815 ret = q.scalar()
816 if ret is None:
816 if ret is None:
817 q = UserEmailMap.query()
817 q = UserEmailMap.query()
818 # try fetching in alternate email map
818 # try fetching in alternate email map
819 if case_insensitive:
819 if case_insensitive:
820 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
820 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
821 else:
821 else:
822 q = q.filter(UserEmailMap.email == email)
822 q = q.filter(UserEmailMap.email == email)
823 q = q.options(joinedload(UserEmailMap.user))
823 q = q.options(joinedload(UserEmailMap.user))
824 if cache:
824 if cache:
825 q = q.options(
825 q = q.options(
826 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
826 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
827 ret = getattr(q.scalar(), 'user', None)
827 ret = getattr(q.scalar(), 'user', None)
828
828
829 return ret
829 return ret
830
830
831 @classmethod
831 @classmethod
832 def get_from_cs_author(cls, author):
832 def get_from_cs_author(cls, author):
833 """
833 """
834 Tries to get User objects out of commit author string
834 Tries to get User objects out of commit author string
835
835
836 :param author:
836 :param author:
837 """
837 """
838 from rhodecode.lib.helpers import email, author_name
838 from rhodecode.lib.helpers import email, author_name
839 # Valid email in the attribute passed, see if they're in the system
839 # Valid email in the attribute passed, see if they're in the system
840 _email = email(author)
840 _email = email(author)
841 if _email:
841 if _email:
842 user = cls.get_by_email(_email, case_insensitive=True)
842 user = cls.get_by_email(_email, case_insensitive=True)
843 if user:
843 if user:
844 return user
844 return user
845 # Maybe we can match by username?
845 # Maybe we can match by username?
846 _author = author_name(author)
846 _author = author_name(author)
847 user = cls.get_by_username(_author, case_insensitive=True)
847 user = cls.get_by_username(_author, case_insensitive=True)
848 if user:
848 if user:
849 return user
849 return user
850
850
851 def update_userdata(self, **kwargs):
851 def update_userdata(self, **kwargs):
852 usr = self
852 usr = self
853 old = usr.user_data
853 old = usr.user_data
854 old.update(**kwargs)
854 old.update(**kwargs)
855 usr.user_data = old
855 usr.user_data = old
856 Session().add(usr)
856 Session().add(usr)
857 log.debug('updated userdata with ', kwargs)
857 log.debug('updated userdata with ', kwargs)
858
858
859 def update_lastlogin(self):
859 def update_lastlogin(self):
860 """Update user lastlogin"""
860 """Update user lastlogin"""
861 self.last_login = datetime.datetime.now()
861 self.last_login = datetime.datetime.now()
862 Session().add(self)
862 Session().add(self)
863 log.debug('updated user %s lastlogin', self.username)
863 log.debug('updated user %s lastlogin', self.username)
864
864
865 def update_lastactivity(self):
865 def update_lastactivity(self):
866 """Update user lastactivity"""
866 """Update user lastactivity"""
867 self.last_activity = datetime.datetime.now()
867 self.last_activity = datetime.datetime.now()
868 Session().add(self)
868 Session().add(self)
869 log.debug('updated user %s lastactivity', self.username)
869 log.debug('updated user %s lastactivity', self.username)
870
870
871 def update_password(self, new_password):
871 def update_password(self, new_password):
872 from rhodecode.lib.auth import get_crypt_password
872 from rhodecode.lib.auth import get_crypt_password
873
873
874 self.password = get_crypt_password(new_password)
874 self.password = get_crypt_password(new_password)
875 Session().add(self)
875 Session().add(self)
876
876
877 @classmethod
877 @classmethod
878 def get_first_super_admin(cls):
878 def get_first_super_admin(cls):
879 user = User.query().filter(User.admin == true()).first()
879 user = User.query().filter(User.admin == true()).first()
880 if user is None:
880 if user is None:
881 raise Exception('FATAL: Missing administrative account!')
881 raise Exception('FATAL: Missing administrative account!')
882 return user
882 return user
883
883
884 @classmethod
884 @classmethod
885 def get_all_super_admins(cls):
885 def get_all_super_admins(cls):
886 """
886 """
887 Returns all admin accounts sorted by username
887 Returns all admin accounts sorted by username
888 """
888 """
889 return User.query().filter(User.admin == true())\
889 return User.query().filter(User.admin == true())\
890 .order_by(User.username.asc()).all()
890 .order_by(User.username.asc()).all()
891
891
892 @classmethod
892 @classmethod
893 def get_default_user(cls, cache=False, refresh=False):
893 def get_default_user(cls, cache=False, refresh=False):
894 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
894 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
895 if user is None:
895 if user is None:
896 raise Exception('FATAL: Missing default account!')
896 raise Exception('FATAL: Missing default account!')
897 if refresh:
897 if refresh:
898 # The default user might be based on outdated state which
898 # The default user might be based on outdated state which
899 # has been loaded from the cache.
899 # has been loaded from the cache.
900 # A call to refresh() ensures that the
900 # A call to refresh() ensures that the
901 # latest state from the database is used.
901 # latest state from the database is used.
902 Session().refresh(user)
902 Session().refresh(user)
903 return user
903 return user
904
904
905 def _get_default_perms(self, user, suffix=''):
905 def _get_default_perms(self, user, suffix=''):
906 from rhodecode.model.permission import PermissionModel
906 from rhodecode.model.permission import PermissionModel
907 return PermissionModel().get_default_perms(user.user_perms, suffix)
907 return PermissionModel().get_default_perms(user.user_perms, suffix)
908
908
909 def get_default_perms(self, suffix=''):
909 def get_default_perms(self, suffix=''):
910 return self._get_default_perms(self, suffix)
910 return self._get_default_perms(self, suffix)
911
911
912 def get_api_data(self, include_secrets=False, details='full'):
912 def get_api_data(self, include_secrets=False, details='full'):
913 """
913 """
914 Common function for generating user related data for API
914 Common function for generating user related data for API
915
915
916 :param include_secrets: By default secrets in the API data will be replaced
916 :param include_secrets: By default secrets in the API data will be replaced
917 by a placeholder value to prevent exposing this data by accident. In case
917 by a placeholder value to prevent exposing this data by accident. In case
918 this data shall be exposed, set this flag to ``True``.
918 this data shall be exposed, set this flag to ``True``.
919
919
920 :param details: details can be 'basic|full' basic gives only a subset of
920 :param details: details can be 'basic|full' basic gives only a subset of
921 the available user information that includes user_id, name and emails.
921 the available user information that includes user_id, name and emails.
922 """
922 """
923 user = self
923 user = self
924 user_data = self.user_data
924 user_data = self.user_data
925 data = {
925 data = {
926 'user_id': user.user_id,
926 'user_id': user.user_id,
927 'username': user.username,
927 'username': user.username,
928 'firstname': user.name,
928 'firstname': user.name,
929 'lastname': user.lastname,
929 'lastname': user.lastname,
930 'email': user.email,
930 'email': user.email,
931 'emails': user.emails,
931 'emails': user.emails,
932 }
932 }
933 if details == 'basic':
933 if details == 'basic':
934 return data
934 return data
935
935
936 auth_token_length = 40
936 auth_token_length = 40
937 auth_token_replacement = '*' * auth_token_length
937 auth_token_replacement = '*' * auth_token_length
938
938
939 extras = {
939 extras = {
940 'auth_tokens': [auth_token_replacement],
940 'auth_tokens': [auth_token_replacement],
941 'active': user.active,
941 'active': user.active,
942 'admin': user.admin,
942 'admin': user.admin,
943 'extern_type': user.extern_type,
943 'extern_type': user.extern_type,
944 'extern_name': user.extern_name,
944 'extern_name': user.extern_name,
945 'last_login': user.last_login,
945 'last_login': user.last_login,
946 'last_activity': user.last_activity,
946 'last_activity': user.last_activity,
947 'ip_addresses': user.ip_addresses,
947 'ip_addresses': user.ip_addresses,
948 'language': user_data.get('language')
948 'language': user_data.get('language')
949 }
949 }
950 data.update(extras)
950 data.update(extras)
951
951
952 if include_secrets:
952 if include_secrets:
953 data['auth_tokens'] = user.auth_tokens
953 data['auth_tokens'] = user.auth_tokens
954 return data
954 return data
955
955
956 def __json__(self):
956 def __json__(self):
957 data = {
957 data = {
958 'full_name': self.full_name,
958 'full_name': self.full_name,
959 'full_name_or_username': self.full_name_or_username,
959 'full_name_or_username': self.full_name_or_username,
960 'short_contact': self.short_contact,
960 'short_contact': self.short_contact,
961 'full_contact': self.full_contact,
961 'full_contact': self.full_contact,
962 }
962 }
963 data.update(self.get_api_data())
963 data.update(self.get_api_data())
964 return data
964 return data
965
965
966
966
967 class UserApiKeys(Base, BaseModel):
967 class UserApiKeys(Base, BaseModel):
968 __tablename__ = 'user_api_keys'
968 __tablename__ = 'user_api_keys'
969 __table_args__ = (
969 __table_args__ = (
970 Index('uak_api_key_idx', 'api_key', unique=True),
970 Index('uak_api_key_idx', 'api_key', unique=True),
971 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
971 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
972 {'extend_existing': True, 'mysql_engine': 'InnoDB',
972 {'extend_existing': True, 'mysql_engine': 'InnoDB',
973 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
973 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
974 )
974 )
975 __mapper_args__ = {}
975 __mapper_args__ = {}
976
976
977 # ApiKey role
977 # ApiKey role
978 ROLE_ALL = 'token_role_all'
978 ROLE_ALL = 'token_role_all'
979 ROLE_HTTP = 'token_role_http'
979 ROLE_HTTP = 'token_role_http'
980 ROLE_VCS = 'token_role_vcs'
980 ROLE_VCS = 'token_role_vcs'
981 ROLE_API = 'token_role_api'
981 ROLE_API = 'token_role_api'
982 ROLE_FEED = 'token_role_feed'
982 ROLE_FEED = 'token_role_feed'
983 ROLE_PASSWORD_RESET = 'token_password_reset'
983 ROLE_PASSWORD_RESET = 'token_password_reset'
984
984
985 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
985 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
986
986
987 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
987 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
988 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
988 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
989 api_key = Column("api_key", String(255), nullable=False, unique=True)
989 api_key = Column("api_key", String(255), nullable=False, unique=True)
990 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
990 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
991 expires = Column('expires', Float(53), nullable=False)
991 expires = Column('expires', Float(53), nullable=False)
992 role = Column('role', String(255), nullable=True)
992 role = Column('role', String(255), nullable=True)
993 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
993 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
994
994
995 # scope columns
995 # scope columns
996 repo_id = Column(
996 repo_id = Column(
997 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
997 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
998 nullable=True, unique=None, default=None)
998 nullable=True, unique=None, default=None)
999 repo = relationship('Repository', lazy='joined')
999 repo = relationship('Repository', lazy='joined')
1000
1000
1001 repo_group_id = Column(
1001 repo_group_id = Column(
1002 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1002 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1003 nullable=True, unique=None, default=None)
1003 nullable=True, unique=None, default=None)
1004 repo_group = relationship('RepoGroup', lazy='joined')
1004 repo_group = relationship('RepoGroup', lazy='joined')
1005
1005
1006 user = relationship('User', lazy='joined')
1006 user = relationship('User', lazy='joined')
1007
1007
1008 def __unicode__(self):
1008 def __unicode__(self):
1009 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1009 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1010
1010
1011 def __json__(self):
1011 def __json__(self):
1012 data = {
1012 data = {
1013 'auth_token': self.api_key,
1013 'auth_token': self.api_key,
1014 'role': self.role,
1014 'role': self.role,
1015 'scope': self.scope_humanized,
1015 'scope': self.scope_humanized,
1016 'expired': self.expired
1016 'expired': self.expired
1017 }
1017 }
1018 return data
1018 return data
1019
1019
1020 def get_api_data(self, include_secrets=False):
1020 def get_api_data(self, include_secrets=False):
1021 data = self.__json__()
1021 data = self.__json__()
1022 if include_secrets:
1022 if include_secrets:
1023 return data
1023 return data
1024 else:
1024 else:
1025 data['auth_token'] = self.token_obfuscated
1025 data['auth_token'] = self.token_obfuscated
1026 return data
1026 return data
1027
1027
1028 @hybrid_property
1028 @hybrid_property
1029 def description_safe(self):
1029 def description_safe(self):
1030 from rhodecode.lib import helpers as h
1030 from rhodecode.lib import helpers as h
1031 return h.escape(self.description)
1031 return h.escape(self.description)
1032
1032
1033 @property
1033 @property
1034 def expired(self):
1034 def expired(self):
1035 if self.expires == -1:
1035 if self.expires == -1:
1036 return False
1036 return False
1037 return time.time() > self.expires
1037 return time.time() > self.expires
1038
1038
1039 @classmethod
1039 @classmethod
1040 def _get_role_name(cls, role):
1040 def _get_role_name(cls, role):
1041 return {
1041 return {
1042 cls.ROLE_ALL: _('all'),
1042 cls.ROLE_ALL: _('all'),
1043 cls.ROLE_HTTP: _('http/web interface'),
1043 cls.ROLE_HTTP: _('http/web interface'),
1044 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1044 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1045 cls.ROLE_API: _('api calls'),
1045 cls.ROLE_API: _('api calls'),
1046 cls.ROLE_FEED: _('feed access'),
1046 cls.ROLE_FEED: _('feed access'),
1047 }.get(role, role)
1047 }.get(role, role)
1048
1048
1049 @property
1049 @property
1050 def role_humanized(self):
1050 def role_humanized(self):
1051 return self._get_role_name(self.role)
1051 return self._get_role_name(self.role)
1052
1052
1053 def _get_scope(self):
1053 def _get_scope(self):
1054 if self.repo:
1054 if self.repo:
1055 return repr(self.repo)
1055 return repr(self.repo)
1056 if self.repo_group:
1056 if self.repo_group:
1057 return repr(self.repo_group) + ' (recursive)'
1057 return repr(self.repo_group) + ' (recursive)'
1058 return 'global'
1058 return 'global'
1059
1059
1060 @property
1060 @property
1061 def scope_humanized(self):
1061 def scope_humanized(self):
1062 return self._get_scope()
1062 return self._get_scope()
1063
1063
1064 @property
1064 @property
1065 def token_obfuscated(self):
1065 def token_obfuscated(self):
1066 if self.api_key:
1066 if self.api_key:
1067 return self.api_key[:4] + "****"
1067 return self.api_key[:4] + "****"
1068
1068
1069
1069
1070 class UserEmailMap(Base, BaseModel):
1070 class UserEmailMap(Base, BaseModel):
1071 __tablename__ = 'user_email_map'
1071 __tablename__ = 'user_email_map'
1072 __table_args__ = (
1072 __table_args__ = (
1073 Index('uem_email_idx', 'email'),
1073 Index('uem_email_idx', 'email'),
1074 UniqueConstraint('email'),
1074 UniqueConstraint('email'),
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1077 )
1077 )
1078 __mapper_args__ = {}
1078 __mapper_args__ = {}
1079
1079
1080 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1080 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1082 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1082 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1083 user = relationship('User', lazy='joined')
1083 user = relationship('User', lazy='joined')
1084
1084
1085 @validates('_email')
1085 @validates('_email')
1086 def validate_email(self, key, email):
1086 def validate_email(self, key, email):
1087 # check if this email is not main one
1087 # check if this email is not main one
1088 main_email = Session().query(User).filter(User.email == email).scalar()
1088 main_email = Session().query(User).filter(User.email == email).scalar()
1089 if main_email is not None:
1089 if main_email is not None:
1090 raise AttributeError('email %s is present is user table' % email)
1090 raise AttributeError('email %s is present is user table' % email)
1091 return email
1091 return email
1092
1092
1093 @hybrid_property
1093 @hybrid_property
1094 def email(self):
1094 def email(self):
1095 return self._email
1095 return self._email
1096
1096
1097 @email.setter
1097 @email.setter
1098 def email(self, val):
1098 def email(self, val):
1099 self._email = val.lower() if val else None
1099 self._email = val.lower() if val else None
1100
1100
1101
1101
1102 class UserIpMap(Base, BaseModel):
1102 class UserIpMap(Base, BaseModel):
1103 __tablename__ = 'user_ip_map'
1103 __tablename__ = 'user_ip_map'
1104 __table_args__ = (
1104 __table_args__ = (
1105 UniqueConstraint('user_id', 'ip_addr'),
1105 UniqueConstraint('user_id', 'ip_addr'),
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1108 )
1108 )
1109 __mapper_args__ = {}
1109 __mapper_args__ = {}
1110
1110
1111 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1111 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1112 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1112 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1113 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1113 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1114 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1114 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1115 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1115 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1116 user = relationship('User', lazy='joined')
1116 user = relationship('User', lazy='joined')
1117
1117
1118 @hybrid_property
1118 @hybrid_property
1119 def description_safe(self):
1119 def description_safe(self):
1120 from rhodecode.lib import helpers as h
1120 from rhodecode.lib import helpers as h
1121 return h.escape(self.description)
1121 return h.escape(self.description)
1122
1122
1123 @classmethod
1123 @classmethod
1124 def _get_ip_range(cls, ip_addr):
1124 def _get_ip_range(cls, ip_addr):
1125 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1125 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1126 return [str(net.network_address), str(net.broadcast_address)]
1126 return [str(net.network_address), str(net.broadcast_address)]
1127
1127
1128 def __json__(self):
1128 def __json__(self):
1129 return {
1129 return {
1130 'ip_addr': self.ip_addr,
1130 'ip_addr': self.ip_addr,
1131 'ip_range': self._get_ip_range(self.ip_addr),
1131 'ip_range': self._get_ip_range(self.ip_addr),
1132 }
1132 }
1133
1133
1134 def __unicode__(self):
1134 def __unicode__(self):
1135 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1135 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1136 self.user_id, self.ip_addr)
1136 self.user_id, self.ip_addr)
1137
1137
1138
1138
1139 class UserLog(Base, BaseModel):
1139 class UserLog(Base, BaseModel):
1140 __tablename__ = 'user_logs'
1140 __tablename__ = 'user_logs'
1141 __table_args__ = (
1141 __table_args__ = (
1142 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1142 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1143 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1143 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1144 )
1144 )
1145 VERSION_1 = 'v1'
1145 VERSION_1 = 'v1'
1146 VERSION_2 = 'v2'
1146 VERSION_2 = 'v2'
1147 VERSIONS = [VERSION_1, VERSION_2]
1147 VERSIONS = [VERSION_1, VERSION_2]
1148
1148
1149 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1149 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1150 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1150 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1151 username = Column("username", String(255), nullable=True, unique=None, default=None)
1151 username = Column("username", String(255), nullable=True, unique=None, default=None)
1152 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1152 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1153 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1153 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1154 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1154 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1155 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1155 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1156 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1156 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1157
1157
1158 version = Column("version", String(255), nullable=True, default=VERSION_1)
1158 version = Column("version", String(255), nullable=True, default=VERSION_1)
1159 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1159 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1160 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1160 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1161
1161
1162 def __unicode__(self):
1162 def __unicode__(self):
1163 return u"<%s('id:%s:%s')>" % (
1163 return u"<%s('id:%s:%s')>" % (
1164 self.__class__.__name__, self.repository_name, self.action)
1164 self.__class__.__name__, self.repository_name, self.action)
1165
1165
1166 def __json__(self):
1166 def __json__(self):
1167 return {
1167 return {
1168 'user_id': self.user_id,
1168 'user_id': self.user_id,
1169 'username': self.username,
1169 'username': self.username,
1170 'repository_id': self.repository_id,
1170 'repository_id': self.repository_id,
1171 'repository_name': self.repository_name,
1171 'repository_name': self.repository_name,
1172 'user_ip': self.user_ip,
1172 'user_ip': self.user_ip,
1173 'action_date': self.action_date,
1173 'action_date': self.action_date,
1174 'action': self.action,
1174 'action': self.action,
1175 }
1175 }
1176
1176
1177 @property
1177 @property
1178 def action_as_day(self):
1178 def action_as_day(self):
1179 return datetime.date(*self.action_date.timetuple()[:3])
1179 return datetime.date(*self.action_date.timetuple()[:3])
1180
1180
1181 user = relationship('User')
1181 user = relationship('User')
1182 repository = relationship('Repository', cascade='')
1182 repository = relationship('Repository', cascade='')
1183
1183
1184
1184
1185 class UserGroup(Base, BaseModel):
1185 class UserGroup(Base, BaseModel):
1186 __tablename__ = 'users_groups'
1186 __tablename__ = 'users_groups'
1187 __table_args__ = (
1187 __table_args__ = (
1188 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1188 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1189 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1189 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1190 )
1190 )
1191
1191
1192 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1192 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1193 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1193 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1194 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1194 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1195 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1195 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1196 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1196 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1197 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1197 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1198 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1198 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1199 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1199 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1200
1200
1201 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1201 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1202 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1202 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1203 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1203 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1204 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1204 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1205 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1205 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1206 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1206 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1207
1207
1208 user = relationship('User')
1208 user = relationship('User')
1209
1209
1210 @hybrid_property
1210 @hybrid_property
1211 def description_safe(self):
1211 def description_safe(self):
1212 from rhodecode.lib import helpers as h
1212 from rhodecode.lib import helpers as h
1213 return h.escape(self.description)
1213 return h.escape(self.description)
1214
1214
1215 @hybrid_property
1215 @hybrid_property
1216 def group_data(self):
1216 def group_data(self):
1217 if not self._group_data:
1217 if not self._group_data:
1218 return {}
1218 return {}
1219
1219
1220 try:
1220 try:
1221 return json.loads(self._group_data)
1221 return json.loads(self._group_data)
1222 except TypeError:
1222 except TypeError:
1223 return {}
1223 return {}
1224
1224
1225 @group_data.setter
1225 @group_data.setter
1226 def group_data(self, val):
1226 def group_data(self, val):
1227 try:
1227 try:
1228 self._group_data = json.dumps(val)
1228 self._group_data = json.dumps(val)
1229 except Exception:
1229 except Exception:
1230 log.error(traceback.format_exc())
1230 log.error(traceback.format_exc())
1231
1231
1232 def __unicode__(self):
1232 def __unicode__(self):
1233 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1233 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1234 self.users_group_id,
1234 self.users_group_id,
1235 self.users_group_name)
1235 self.users_group_name)
1236
1236
1237 @classmethod
1237 @classmethod
1238 def get_by_group_name(cls, group_name, cache=False,
1238 def get_by_group_name(cls, group_name, cache=False,
1239 case_insensitive=False):
1239 case_insensitive=False):
1240 if case_insensitive:
1240 if case_insensitive:
1241 q = cls.query().filter(func.lower(cls.users_group_name) ==
1241 q = cls.query().filter(func.lower(cls.users_group_name) ==
1242 func.lower(group_name))
1242 func.lower(group_name))
1243
1243
1244 else:
1244 else:
1245 q = cls.query().filter(cls.users_group_name == group_name)
1245 q = cls.query().filter(cls.users_group_name == group_name)
1246 if cache:
1246 if cache:
1247 q = q.options(
1247 q = q.options(
1248 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1248 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1249 return q.scalar()
1249 return q.scalar()
1250
1250
1251 @classmethod
1251 @classmethod
1252 def get(cls, user_group_id, cache=False):
1252 def get(cls, user_group_id, cache=False):
1253 user_group = cls.query()
1253 user_group = cls.query()
1254 if cache:
1254 if cache:
1255 user_group = user_group.options(
1255 user_group = user_group.options(
1256 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1256 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1257 return user_group.get(user_group_id)
1257 return user_group.get(user_group_id)
1258
1258
1259 def permissions(self, with_admins=True, with_owner=True):
1259 def permissions(self, with_admins=True, with_owner=True):
1260 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1260 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1261 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1261 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1262 joinedload(UserUserGroupToPerm.user),
1262 joinedload(UserUserGroupToPerm.user),
1263 joinedload(UserUserGroupToPerm.permission),)
1263 joinedload(UserUserGroupToPerm.permission),)
1264
1264
1265 # get owners and admins and permissions. We do a trick of re-writing
1265 # get owners and admins and permissions. We do a trick of re-writing
1266 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1266 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1267 # has a global reference and changing one object propagates to all
1267 # has a global reference and changing one object propagates to all
1268 # others. This means if admin is also an owner admin_row that change
1268 # others. This means if admin is also an owner admin_row that change
1269 # would propagate to both objects
1269 # would propagate to both objects
1270 perm_rows = []
1270 perm_rows = []
1271 for _usr in q.all():
1271 for _usr in q.all():
1272 usr = AttributeDict(_usr.user.get_dict())
1272 usr = AttributeDict(_usr.user.get_dict())
1273 usr.permission = _usr.permission.permission_name
1273 usr.permission = _usr.permission.permission_name
1274 perm_rows.append(usr)
1274 perm_rows.append(usr)
1275
1275
1276 # filter the perm rows by 'default' first and then sort them by
1276 # filter the perm rows by 'default' first and then sort them by
1277 # admin,write,read,none permissions sorted again alphabetically in
1277 # admin,write,read,none permissions sorted again alphabetically in
1278 # each group
1278 # each group
1279 perm_rows = sorted(perm_rows, key=display_sort)
1279 perm_rows = sorted(perm_rows, key=display_sort)
1280
1280
1281 _admin_perm = 'usergroup.admin'
1281 _admin_perm = 'usergroup.admin'
1282 owner_row = []
1282 owner_row = []
1283 if with_owner:
1283 if with_owner:
1284 usr = AttributeDict(self.user.get_dict())
1284 usr = AttributeDict(self.user.get_dict())
1285 usr.owner_row = True
1285 usr.owner_row = True
1286 usr.permission = _admin_perm
1286 usr.permission = _admin_perm
1287 owner_row.append(usr)
1287 owner_row.append(usr)
1288
1288
1289 super_admin_rows = []
1289 super_admin_rows = []
1290 if with_admins:
1290 if with_admins:
1291 for usr in User.get_all_super_admins():
1291 for usr in User.get_all_super_admins():
1292 # if this admin is also owner, don't double the record
1292 # if this admin is also owner, don't double the record
1293 if usr.user_id == owner_row[0].user_id:
1293 if usr.user_id == owner_row[0].user_id:
1294 owner_row[0].admin_row = True
1294 owner_row[0].admin_row = True
1295 else:
1295 else:
1296 usr = AttributeDict(usr.get_dict())
1296 usr = AttributeDict(usr.get_dict())
1297 usr.admin_row = True
1297 usr.admin_row = True
1298 usr.permission = _admin_perm
1298 usr.permission = _admin_perm
1299 super_admin_rows.append(usr)
1299 super_admin_rows.append(usr)
1300
1300
1301 return super_admin_rows + owner_row + perm_rows
1301 return super_admin_rows + owner_row + perm_rows
1302
1302
1303 def permission_user_groups(self):
1303 def permission_user_groups(self):
1304 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1304 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1305 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1305 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1306 joinedload(UserGroupUserGroupToPerm.target_user_group),
1306 joinedload(UserGroupUserGroupToPerm.target_user_group),
1307 joinedload(UserGroupUserGroupToPerm.permission),)
1307 joinedload(UserGroupUserGroupToPerm.permission),)
1308
1308
1309 perm_rows = []
1309 perm_rows = []
1310 for _user_group in q.all():
1310 for _user_group in q.all():
1311 usr = AttributeDict(_user_group.user_group.get_dict())
1311 usr = AttributeDict(_user_group.user_group.get_dict())
1312 usr.permission = _user_group.permission.permission_name
1312 usr.permission = _user_group.permission.permission_name
1313 perm_rows.append(usr)
1313 perm_rows.append(usr)
1314
1314
1315 return perm_rows
1315 return perm_rows
1316
1316
1317 def _get_default_perms(self, user_group, suffix=''):
1317 def _get_default_perms(self, user_group, suffix=''):
1318 from rhodecode.model.permission import PermissionModel
1318 from rhodecode.model.permission import PermissionModel
1319 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1319 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1320
1320
1321 def get_default_perms(self, suffix=''):
1321 def get_default_perms(self, suffix=''):
1322 return self._get_default_perms(self, suffix)
1322 return self._get_default_perms(self, suffix)
1323
1323
1324 def get_api_data(self, with_group_members=True, include_secrets=False):
1324 def get_api_data(self, with_group_members=True, include_secrets=False):
1325 """
1325 """
1326 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1326 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1327 basically forwarded.
1327 basically forwarded.
1328
1328
1329 """
1329 """
1330 user_group = self
1330 user_group = self
1331 data = {
1331 data = {
1332 'users_group_id': user_group.users_group_id,
1332 'users_group_id': user_group.users_group_id,
1333 'group_name': user_group.users_group_name,
1333 'group_name': user_group.users_group_name,
1334 'group_description': user_group.user_group_description,
1334 'group_description': user_group.user_group_description,
1335 'active': user_group.users_group_active,
1335 'active': user_group.users_group_active,
1336 'owner': user_group.user.username,
1336 'owner': user_group.user.username,
1337 'owner_email': user_group.user.email,
1337 'owner_email': user_group.user.email,
1338 }
1338 }
1339
1339
1340 if with_group_members:
1340 if with_group_members:
1341 users = []
1341 users = []
1342 for user in user_group.members:
1342 for user in user_group.members:
1343 user = user.user
1343 user = user.user
1344 users.append(user.get_api_data(include_secrets=include_secrets))
1344 users.append(user.get_api_data(include_secrets=include_secrets))
1345 data['users'] = users
1345 data['users'] = users
1346
1346
1347 return data
1347 return data
1348
1348
1349
1349
1350 class UserGroupMember(Base, BaseModel):
1350 class UserGroupMember(Base, BaseModel):
1351 __tablename__ = 'users_groups_members'
1351 __tablename__ = 'users_groups_members'
1352 __table_args__ = (
1352 __table_args__ = (
1353 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1353 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1354 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1354 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1355 )
1355 )
1356
1356
1357 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1357 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1358 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1358 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1359 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1359 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1360
1360
1361 user = relationship('User', lazy='joined')
1361 user = relationship('User', lazy='joined')
1362 users_group = relationship('UserGroup')
1362 users_group = relationship('UserGroup')
1363
1363
1364 def __init__(self, gr_id='', u_id=''):
1364 def __init__(self, gr_id='', u_id=''):
1365 self.users_group_id = gr_id
1365 self.users_group_id = gr_id
1366 self.user_id = u_id
1366 self.user_id = u_id
1367
1367
1368
1368
1369 class RepositoryField(Base, BaseModel):
1369 class RepositoryField(Base, BaseModel):
1370 __tablename__ = 'repositories_fields'
1370 __tablename__ = 'repositories_fields'
1371 __table_args__ = (
1371 __table_args__ = (
1372 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1372 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1373 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1373 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1374 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1374 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1375 )
1375 )
1376 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1376 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1377
1377
1378 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1378 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1379 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1379 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1380 field_key = Column("field_key", String(250))
1380 field_key = Column("field_key", String(250))
1381 field_label = Column("field_label", String(1024), nullable=False)
1381 field_label = Column("field_label", String(1024), nullable=False)
1382 field_value = Column("field_value", String(10000), nullable=False)
1382 field_value = Column("field_value", String(10000), nullable=False)
1383 field_desc = Column("field_desc", String(1024), nullable=False)
1383 field_desc = Column("field_desc", String(1024), nullable=False)
1384 field_type = Column("field_type", String(255), nullable=False, unique=None)
1384 field_type = Column("field_type", String(255), nullable=False, unique=None)
1385 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1385 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1386
1386
1387 repository = relationship('Repository')
1387 repository = relationship('Repository')
1388
1388
1389 @property
1389 @property
1390 def field_key_prefixed(self):
1390 def field_key_prefixed(self):
1391 return 'ex_%s' % self.field_key
1391 return 'ex_%s' % self.field_key
1392
1392
1393 @classmethod
1393 @classmethod
1394 def un_prefix_key(cls, key):
1394 def un_prefix_key(cls, key):
1395 if key.startswith(cls.PREFIX):
1395 if key.startswith(cls.PREFIX):
1396 return key[len(cls.PREFIX):]
1396 return key[len(cls.PREFIX):]
1397 return key
1397 return key
1398
1398
1399 @classmethod
1399 @classmethod
1400 def get_by_key_name(cls, key, repo):
1400 def get_by_key_name(cls, key, repo):
1401 row = cls.query()\
1401 row = cls.query()\
1402 .filter(cls.repository == repo)\
1402 .filter(cls.repository == repo)\
1403 .filter(cls.field_key == key).scalar()
1403 .filter(cls.field_key == key).scalar()
1404 return row
1404 return row
1405
1405
1406
1406
1407 class Repository(Base, BaseModel):
1407 class Repository(Base, BaseModel):
1408 __tablename__ = 'repositories'
1408 __tablename__ = 'repositories'
1409 __table_args__ = (
1409 __table_args__ = (
1410 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1410 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1411 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1411 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1412 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1412 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1413 )
1413 )
1414 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1414 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1415 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1415 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1416
1416
1417 STATE_CREATED = 'repo_state_created'
1417 STATE_CREATED = 'repo_state_created'
1418 STATE_PENDING = 'repo_state_pending'
1418 STATE_PENDING = 'repo_state_pending'
1419 STATE_ERROR = 'repo_state_error'
1419 STATE_ERROR = 'repo_state_error'
1420
1420
1421 LOCK_AUTOMATIC = 'lock_auto'
1421 LOCK_AUTOMATIC = 'lock_auto'
1422 LOCK_API = 'lock_api'
1422 LOCK_API = 'lock_api'
1423 LOCK_WEB = 'lock_web'
1423 LOCK_WEB = 'lock_web'
1424 LOCK_PULL = 'lock_pull'
1424 LOCK_PULL = 'lock_pull'
1425
1425
1426 NAME_SEP = URL_SEP
1426 NAME_SEP = URL_SEP
1427
1427
1428 repo_id = Column(
1428 repo_id = Column(
1429 "repo_id", Integer(), nullable=False, unique=True, default=None,
1429 "repo_id", Integer(), nullable=False, unique=True, default=None,
1430 primary_key=True)
1430 primary_key=True)
1431 _repo_name = Column(
1431 _repo_name = Column(
1432 "repo_name", Text(), nullable=False, default=None)
1432 "repo_name", Text(), nullable=False, default=None)
1433 _repo_name_hash = Column(
1433 _repo_name_hash = Column(
1434 "repo_name_hash", String(255), nullable=False, unique=True)
1434 "repo_name_hash", String(255), nullable=False, unique=True)
1435 repo_state = Column("repo_state", String(255), nullable=True)
1435 repo_state = Column("repo_state", String(255), nullable=True)
1436
1436
1437 clone_uri = Column(
1437 clone_uri = Column(
1438 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1438 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1439 default=None)
1439 default=None)
1440 repo_type = Column(
1440 repo_type = Column(
1441 "repo_type", String(255), nullable=False, unique=False, default=None)
1441 "repo_type", String(255), nullable=False, unique=False, default=None)
1442 user_id = Column(
1442 user_id = Column(
1443 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1443 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1444 unique=False, default=None)
1444 unique=False, default=None)
1445 private = Column(
1445 private = Column(
1446 "private", Boolean(), nullable=True, unique=None, default=None)
1446 "private", Boolean(), nullable=True, unique=None, default=None)
1447 enable_statistics = Column(
1447 enable_statistics = Column(
1448 "statistics", Boolean(), nullable=True, unique=None, default=True)
1448 "statistics", Boolean(), nullable=True, unique=None, default=True)
1449 enable_downloads = Column(
1449 enable_downloads = Column(
1450 "downloads", Boolean(), nullable=True, unique=None, default=True)
1450 "downloads", Boolean(), nullable=True, unique=None, default=True)
1451 description = Column(
1451 description = Column(
1452 "description", String(10000), nullable=True, unique=None, default=None)
1452 "description", String(10000), nullable=True, unique=None, default=None)
1453 created_on = Column(
1453 created_on = Column(
1454 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1454 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1455 default=datetime.datetime.now)
1455 default=datetime.datetime.now)
1456 updated_on = Column(
1456 updated_on = Column(
1457 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1457 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1458 default=datetime.datetime.now)
1458 default=datetime.datetime.now)
1459 _landing_revision = Column(
1459 _landing_revision = Column(
1460 "landing_revision", String(255), nullable=False, unique=False,
1460 "landing_revision", String(255), nullable=False, unique=False,
1461 default=None)
1461 default=None)
1462 enable_locking = Column(
1462 enable_locking = Column(
1463 "enable_locking", Boolean(), nullable=False, unique=None,
1463 "enable_locking", Boolean(), nullable=False, unique=None,
1464 default=False)
1464 default=False)
1465 _locked = Column(
1465 _locked = Column(
1466 "locked", String(255), nullable=True, unique=False, default=None)
1466 "locked", String(255), nullable=True, unique=False, default=None)
1467 _changeset_cache = Column(
1467 _changeset_cache = Column(
1468 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1468 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1469
1469
1470 fork_id = Column(
1470 fork_id = Column(
1471 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1471 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1472 nullable=True, unique=False, default=None)
1472 nullable=True, unique=False, default=None)
1473 group_id = Column(
1473 group_id = Column(
1474 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1474 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1475 unique=False, default=None)
1475 unique=False, default=None)
1476
1476
1477 user = relationship('User', lazy='joined')
1477 user = relationship('User', lazy='joined')
1478 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1478 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1479 group = relationship('RepoGroup', lazy='joined')
1479 group = relationship('RepoGroup', lazy='joined')
1480 repo_to_perm = relationship(
1480 repo_to_perm = relationship(
1481 'UserRepoToPerm', cascade='all',
1481 'UserRepoToPerm', cascade='all',
1482 order_by='UserRepoToPerm.repo_to_perm_id')
1482 order_by='UserRepoToPerm.repo_to_perm_id')
1483 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1483 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1484 stats = relationship('Statistics', cascade='all', uselist=False)
1484 stats = relationship('Statistics', cascade='all', uselist=False)
1485
1485
1486 followers = relationship(
1486 followers = relationship(
1487 'UserFollowing',
1487 'UserFollowing',
1488 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1488 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1489 cascade='all')
1489 cascade='all')
1490 extra_fields = relationship(
1490 extra_fields = relationship(
1491 'RepositoryField', cascade="all, delete, delete-orphan")
1491 'RepositoryField', cascade="all, delete, delete-orphan")
1492 logs = relationship('UserLog')
1492 logs = relationship('UserLog')
1493 comments = relationship(
1493 comments = relationship(
1494 'ChangesetComment', cascade="all, delete, delete-orphan")
1494 'ChangesetComment', cascade="all, delete, delete-orphan")
1495 pull_requests_source = relationship(
1495 pull_requests_source = relationship(
1496 'PullRequest',
1496 'PullRequest',
1497 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1497 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1498 cascade="all, delete, delete-orphan")
1498 cascade="all, delete, delete-orphan")
1499 pull_requests_target = relationship(
1499 pull_requests_target = relationship(
1500 'PullRequest',
1500 'PullRequest',
1501 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1501 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1502 cascade="all, delete, delete-orphan")
1502 cascade="all, delete, delete-orphan")
1503 ui = relationship('RepoRhodeCodeUi', cascade="all")
1503 ui = relationship('RepoRhodeCodeUi', cascade="all")
1504 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1504 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1505 integrations = relationship('Integration',
1505 integrations = relationship('Integration',
1506 cascade="all, delete, delete-orphan")
1506 cascade="all, delete, delete-orphan")
1507
1507
1508 def __unicode__(self):
1508 def __unicode__(self):
1509 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1509 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1510 safe_unicode(self.repo_name))
1510 safe_unicode(self.repo_name))
1511
1511
1512 @hybrid_property
1512 @hybrid_property
1513 def description_safe(self):
1513 def description_safe(self):
1514 from rhodecode.lib import helpers as h
1514 from rhodecode.lib import helpers as h
1515 return h.escape(self.description)
1515 return h.escape(self.description)
1516
1516
1517 @hybrid_property
1517 @hybrid_property
1518 def landing_rev(self):
1518 def landing_rev(self):
1519 # always should return [rev_type, rev]
1519 # always should return [rev_type, rev]
1520 if self._landing_revision:
1520 if self._landing_revision:
1521 _rev_info = self._landing_revision.split(':')
1521 _rev_info = self._landing_revision.split(':')
1522 if len(_rev_info) < 2:
1522 if len(_rev_info) < 2:
1523 _rev_info.insert(0, 'rev')
1523 _rev_info.insert(0, 'rev')
1524 return [_rev_info[0], _rev_info[1]]
1524 return [_rev_info[0], _rev_info[1]]
1525 return [None, None]
1525 return [None, None]
1526
1526
1527 @landing_rev.setter
1527 @landing_rev.setter
1528 def landing_rev(self, val):
1528 def landing_rev(self, val):
1529 if ':' not in val:
1529 if ':' not in val:
1530 raise ValueError('value must be delimited with `:` and consist '
1530 raise ValueError('value must be delimited with `:` and consist '
1531 'of <rev_type>:<rev>, got %s instead' % val)
1531 'of <rev_type>:<rev>, got %s instead' % val)
1532 self._landing_revision = val
1532 self._landing_revision = val
1533
1533
1534 @hybrid_property
1534 @hybrid_property
1535 def locked(self):
1535 def locked(self):
1536 if self._locked:
1536 if self._locked:
1537 user_id, timelocked, reason = self._locked.split(':')
1537 user_id, timelocked, reason = self._locked.split(':')
1538 lock_values = int(user_id), timelocked, reason
1538 lock_values = int(user_id), timelocked, reason
1539 else:
1539 else:
1540 lock_values = [None, None, None]
1540 lock_values = [None, None, None]
1541 return lock_values
1541 return lock_values
1542
1542
1543 @locked.setter
1543 @locked.setter
1544 def locked(self, val):
1544 def locked(self, val):
1545 if val and isinstance(val, (list, tuple)):
1545 if val and isinstance(val, (list, tuple)):
1546 self._locked = ':'.join(map(str, val))
1546 self._locked = ':'.join(map(str, val))
1547 else:
1547 else:
1548 self._locked = None
1548 self._locked = None
1549
1549
1550 @hybrid_property
1550 @hybrid_property
1551 def changeset_cache(self):
1551 def changeset_cache(self):
1552 from rhodecode.lib.vcs.backends.base import EmptyCommit
1552 from rhodecode.lib.vcs.backends.base import EmptyCommit
1553 dummy = EmptyCommit().__json__()
1553 dummy = EmptyCommit().__json__()
1554 if not self._changeset_cache:
1554 if not self._changeset_cache:
1555 return dummy
1555 return dummy
1556 try:
1556 try:
1557 return json.loads(self._changeset_cache)
1557 return json.loads(self._changeset_cache)
1558 except TypeError:
1558 except TypeError:
1559 return dummy
1559 return dummy
1560 except Exception:
1560 except Exception:
1561 log.error(traceback.format_exc())
1561 log.error(traceback.format_exc())
1562 return dummy
1562 return dummy
1563
1563
1564 @changeset_cache.setter
1564 @changeset_cache.setter
1565 def changeset_cache(self, val):
1565 def changeset_cache(self, val):
1566 try:
1566 try:
1567 self._changeset_cache = json.dumps(val)
1567 self._changeset_cache = json.dumps(val)
1568 except Exception:
1568 except Exception:
1569 log.error(traceback.format_exc())
1569 log.error(traceback.format_exc())
1570
1570
1571 @hybrid_property
1571 @hybrid_property
1572 def repo_name(self):
1572 def repo_name(self):
1573 return self._repo_name
1573 return self._repo_name
1574
1574
1575 @repo_name.setter
1575 @repo_name.setter
1576 def repo_name(self, value):
1576 def repo_name(self, value):
1577 self._repo_name = value
1577 self._repo_name = value
1578 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1578 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1579
1579
1580 @classmethod
1580 @classmethod
1581 def normalize_repo_name(cls, repo_name):
1581 def normalize_repo_name(cls, repo_name):
1582 """
1582 """
1583 Normalizes os specific repo_name to the format internally stored inside
1583 Normalizes os specific repo_name to the format internally stored inside
1584 database using URL_SEP
1584 database using URL_SEP
1585
1585
1586 :param cls:
1586 :param cls:
1587 :param repo_name:
1587 :param repo_name:
1588 """
1588 """
1589 return cls.NAME_SEP.join(repo_name.split(os.sep))
1589 return cls.NAME_SEP.join(repo_name.split(os.sep))
1590
1590
1591 @classmethod
1591 @classmethod
1592 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1592 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1593 session = Session()
1593 session = Session()
1594 q = session.query(cls).filter(cls.repo_name == repo_name)
1594 q = session.query(cls).filter(cls.repo_name == repo_name)
1595
1595
1596 if cache:
1596 if cache:
1597 if identity_cache:
1597 if identity_cache:
1598 val = cls.identity_cache(session, 'repo_name', repo_name)
1598 val = cls.identity_cache(session, 'repo_name', repo_name)
1599 if val:
1599 if val:
1600 return val
1600 return val
1601 else:
1601 else:
1602 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1602 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1603 q = q.options(
1603 q = q.options(
1604 FromCache("sql_cache_short", cache_key))
1604 FromCache("sql_cache_short", cache_key))
1605
1605
1606 return q.scalar()
1606 return q.scalar()
1607
1607
1608 @classmethod
1608 @classmethod
1609 def get_by_full_path(cls, repo_full_path):
1609 def get_by_full_path(cls, repo_full_path):
1610 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1610 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1611 repo_name = cls.normalize_repo_name(repo_name)
1611 repo_name = cls.normalize_repo_name(repo_name)
1612 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1612 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1613
1613
1614 @classmethod
1614 @classmethod
1615 def get_repo_forks(cls, repo_id):
1615 def get_repo_forks(cls, repo_id):
1616 return cls.query().filter(Repository.fork_id == repo_id)
1616 return cls.query().filter(Repository.fork_id == repo_id)
1617
1617
1618 @classmethod
1618 @classmethod
1619 def base_path(cls):
1619 def base_path(cls):
1620 """
1620 """
1621 Returns base path when all repos are stored
1621 Returns base path when all repos are stored
1622
1622
1623 :param cls:
1623 :param cls:
1624 """
1624 """
1625 q = Session().query(RhodeCodeUi)\
1625 q = Session().query(RhodeCodeUi)\
1626 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1626 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1627 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1627 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1628 return q.one().ui_value
1628 return q.one().ui_value
1629
1629
1630 @classmethod
1630 @classmethod
1631 def is_valid(cls, repo_name):
1631 def is_valid(cls, repo_name):
1632 """
1632 """
1633 returns True if given repo name is a valid filesystem repository
1633 returns True if given repo name is a valid filesystem repository
1634
1634
1635 :param cls:
1635 :param cls:
1636 :param repo_name:
1636 :param repo_name:
1637 """
1637 """
1638 from rhodecode.lib.utils import is_valid_repo
1638 from rhodecode.lib.utils import is_valid_repo
1639
1639
1640 return is_valid_repo(repo_name, cls.base_path())
1640 return is_valid_repo(repo_name, cls.base_path())
1641
1641
1642 @classmethod
1642 @classmethod
1643 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1643 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1644 case_insensitive=True):
1644 case_insensitive=True):
1645 q = Repository.query()
1645 q = Repository.query()
1646
1646
1647 if not isinstance(user_id, Optional):
1647 if not isinstance(user_id, Optional):
1648 q = q.filter(Repository.user_id == user_id)
1648 q = q.filter(Repository.user_id == user_id)
1649
1649
1650 if not isinstance(group_id, Optional):
1650 if not isinstance(group_id, Optional):
1651 q = q.filter(Repository.group_id == group_id)
1651 q = q.filter(Repository.group_id == group_id)
1652
1652
1653 if case_insensitive:
1653 if case_insensitive:
1654 q = q.order_by(func.lower(Repository.repo_name))
1654 q = q.order_by(func.lower(Repository.repo_name))
1655 else:
1655 else:
1656 q = q.order_by(Repository.repo_name)
1656 q = q.order_by(Repository.repo_name)
1657 return q.all()
1657 return q.all()
1658
1658
1659 @property
1659 @property
1660 def forks(self):
1660 def forks(self):
1661 """
1661 """
1662 Return forks of this repo
1662 Return forks of this repo
1663 """
1663 """
1664 return Repository.get_repo_forks(self.repo_id)
1664 return Repository.get_repo_forks(self.repo_id)
1665
1665
1666 @property
1666 @property
1667 def parent(self):
1667 def parent(self):
1668 """
1668 """
1669 Returns fork parent
1669 Returns fork parent
1670 """
1670 """
1671 return self.fork
1671 return self.fork
1672
1672
1673 @property
1673 @property
1674 def just_name(self):
1674 def just_name(self):
1675 return self.repo_name.split(self.NAME_SEP)[-1]
1675 return self.repo_name.split(self.NAME_SEP)[-1]
1676
1676
1677 @property
1677 @property
1678 def groups_with_parents(self):
1678 def groups_with_parents(self):
1679 groups = []
1679 groups = []
1680 if self.group is None:
1680 if self.group is None:
1681 return groups
1681 return groups
1682
1682
1683 cur_gr = self.group
1683 cur_gr = self.group
1684 groups.insert(0, cur_gr)
1684 groups.insert(0, cur_gr)
1685 while 1:
1685 while 1:
1686 gr = getattr(cur_gr, 'parent_group', None)
1686 gr = getattr(cur_gr, 'parent_group', None)
1687 cur_gr = cur_gr.parent_group
1687 cur_gr = cur_gr.parent_group
1688 if gr is None:
1688 if gr is None:
1689 break
1689 break
1690 groups.insert(0, gr)
1690 groups.insert(0, gr)
1691
1691
1692 return groups
1692 return groups
1693
1693
1694 @property
1694 @property
1695 def groups_and_repo(self):
1695 def groups_and_repo(self):
1696 return self.groups_with_parents, self
1696 return self.groups_with_parents, self
1697
1697
1698 @LazyProperty
1698 @LazyProperty
1699 def repo_path(self):
1699 def repo_path(self):
1700 """
1700 """
1701 Returns base full path for that repository means where it actually
1701 Returns base full path for that repository means where it actually
1702 exists on a filesystem
1702 exists on a filesystem
1703 """
1703 """
1704 q = Session().query(RhodeCodeUi).filter(
1704 q = Session().query(RhodeCodeUi).filter(
1705 RhodeCodeUi.ui_key == self.NAME_SEP)
1705 RhodeCodeUi.ui_key == self.NAME_SEP)
1706 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1706 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1707 return q.one().ui_value
1707 return q.one().ui_value
1708
1708
1709 @property
1709 @property
1710 def repo_full_path(self):
1710 def repo_full_path(self):
1711 p = [self.repo_path]
1711 p = [self.repo_path]
1712 # we need to split the name by / since this is how we store the
1712 # we need to split the name by / since this is how we store the
1713 # names in the database, but that eventually needs to be converted
1713 # names in the database, but that eventually needs to be converted
1714 # into a valid system path
1714 # into a valid system path
1715 p += self.repo_name.split(self.NAME_SEP)
1715 p += self.repo_name.split(self.NAME_SEP)
1716 return os.path.join(*map(safe_unicode, p))
1716 return os.path.join(*map(safe_unicode, p))
1717
1717
1718 @property
1718 @property
1719 def cache_keys(self):
1719 def cache_keys(self):
1720 """
1720 """
1721 Returns associated cache keys for that repo
1721 Returns associated cache keys for that repo
1722 """
1722 """
1723 return CacheKey.query()\
1723 return CacheKey.query()\
1724 .filter(CacheKey.cache_args == self.repo_name)\
1724 .filter(CacheKey.cache_args == self.repo_name)\
1725 .order_by(CacheKey.cache_key)\
1725 .order_by(CacheKey.cache_key)\
1726 .all()
1726 .all()
1727
1727
1728 def get_new_name(self, repo_name):
1728 def get_new_name(self, repo_name):
1729 """
1729 """
1730 returns new full repository name based on assigned group and new new
1730 returns new full repository name based on assigned group and new new
1731
1731
1732 :param group_name:
1732 :param group_name:
1733 """
1733 """
1734 path_prefix = self.group.full_path_splitted if self.group else []
1734 path_prefix = self.group.full_path_splitted if self.group else []
1735 return self.NAME_SEP.join(path_prefix + [repo_name])
1735 return self.NAME_SEP.join(path_prefix + [repo_name])
1736
1736
1737 @property
1737 @property
1738 def _config(self):
1738 def _config(self):
1739 """
1739 """
1740 Returns db based config object.
1740 Returns db based config object.
1741 """
1741 """
1742 from rhodecode.lib.utils import make_db_config
1742 from rhodecode.lib.utils import make_db_config
1743 return make_db_config(clear_session=False, repo=self)
1743 return make_db_config(clear_session=False, repo=self)
1744
1744
1745 def permissions(self, with_admins=True, with_owner=True):
1745 def permissions(self, with_admins=True, with_owner=True):
1746 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1746 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1747 q = q.options(joinedload(UserRepoToPerm.repository),
1747 q = q.options(joinedload(UserRepoToPerm.repository),
1748 joinedload(UserRepoToPerm.user),
1748 joinedload(UserRepoToPerm.user),
1749 joinedload(UserRepoToPerm.permission),)
1749 joinedload(UserRepoToPerm.permission),)
1750
1750
1751 # get owners and admins and permissions. We do a trick of re-writing
1751 # get owners and admins and permissions. We do a trick of re-writing
1752 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1752 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1753 # has a global reference and changing one object propagates to all
1753 # has a global reference and changing one object propagates to all
1754 # others. This means if admin is also an owner admin_row that change
1754 # others. This means if admin is also an owner admin_row that change
1755 # would propagate to both objects
1755 # would propagate to both objects
1756 perm_rows = []
1756 perm_rows = []
1757 for _usr in q.all():
1757 for _usr in q.all():
1758 usr = AttributeDict(_usr.user.get_dict())
1758 usr = AttributeDict(_usr.user.get_dict())
1759 usr.permission = _usr.permission.permission_name
1759 usr.permission = _usr.permission.permission_name
1760 perm_rows.append(usr)
1760 perm_rows.append(usr)
1761
1761
1762 # filter the perm rows by 'default' first and then sort them by
1762 # filter the perm rows by 'default' first and then sort them by
1763 # admin,write,read,none permissions sorted again alphabetically in
1763 # admin,write,read,none permissions sorted again alphabetically in
1764 # each group
1764 # each group
1765 perm_rows = sorted(perm_rows, key=display_sort)
1765 perm_rows = sorted(perm_rows, key=display_sort)
1766
1766
1767 _admin_perm = 'repository.admin'
1767 _admin_perm = 'repository.admin'
1768 owner_row = []
1768 owner_row = []
1769 if with_owner:
1769 if with_owner:
1770 usr = AttributeDict(self.user.get_dict())
1770 usr = AttributeDict(self.user.get_dict())
1771 usr.owner_row = True
1771 usr.owner_row = True
1772 usr.permission = _admin_perm
1772 usr.permission = _admin_perm
1773 owner_row.append(usr)
1773 owner_row.append(usr)
1774
1774
1775 super_admin_rows = []
1775 super_admin_rows = []
1776 if with_admins:
1776 if with_admins:
1777 for usr in User.get_all_super_admins():
1777 for usr in User.get_all_super_admins():
1778 # if this admin is also owner, don't double the record
1778 # if this admin is also owner, don't double the record
1779 if usr.user_id == owner_row[0].user_id:
1779 if usr.user_id == owner_row[0].user_id:
1780 owner_row[0].admin_row = True
1780 owner_row[0].admin_row = True
1781 else:
1781 else:
1782 usr = AttributeDict(usr.get_dict())
1782 usr = AttributeDict(usr.get_dict())
1783 usr.admin_row = True
1783 usr.admin_row = True
1784 usr.permission = _admin_perm
1784 usr.permission = _admin_perm
1785 super_admin_rows.append(usr)
1785 super_admin_rows.append(usr)
1786
1786
1787 return super_admin_rows + owner_row + perm_rows
1787 return super_admin_rows + owner_row + perm_rows
1788
1788
1789 def permission_user_groups(self):
1789 def permission_user_groups(self):
1790 q = UserGroupRepoToPerm.query().filter(
1790 q = UserGroupRepoToPerm.query().filter(
1791 UserGroupRepoToPerm.repository == self)
1791 UserGroupRepoToPerm.repository == self)
1792 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1792 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1793 joinedload(UserGroupRepoToPerm.users_group),
1793 joinedload(UserGroupRepoToPerm.users_group),
1794 joinedload(UserGroupRepoToPerm.permission),)
1794 joinedload(UserGroupRepoToPerm.permission),)
1795
1795
1796 perm_rows = []
1796 perm_rows = []
1797 for _user_group in q.all():
1797 for _user_group in q.all():
1798 usr = AttributeDict(_user_group.users_group.get_dict())
1798 usr = AttributeDict(_user_group.users_group.get_dict())
1799 usr.permission = _user_group.permission.permission_name
1799 usr.permission = _user_group.permission.permission_name
1800 perm_rows.append(usr)
1800 perm_rows.append(usr)
1801
1801
1802 return perm_rows
1802 return perm_rows
1803
1803
1804 def get_api_data(self, include_secrets=False):
1804 def get_api_data(self, include_secrets=False):
1805 """
1805 """
1806 Common function for generating repo api data
1806 Common function for generating repo api data
1807
1807
1808 :param include_secrets: See :meth:`User.get_api_data`.
1808 :param include_secrets: See :meth:`User.get_api_data`.
1809
1809
1810 """
1810 """
1811 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1811 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1812 # move this methods on models level.
1812 # move this methods on models level.
1813 from rhodecode.model.settings import SettingsModel
1813 from rhodecode.model.settings import SettingsModel
1814 from rhodecode.model.repo import RepoModel
1814 from rhodecode.model.repo import RepoModel
1815
1815
1816 repo = self
1816 repo = self
1817 _user_id, _time, _reason = self.locked
1817 _user_id, _time, _reason = self.locked
1818
1818
1819 data = {
1819 data = {
1820 'repo_id': repo.repo_id,
1820 'repo_id': repo.repo_id,
1821 'repo_name': repo.repo_name,
1821 'repo_name': repo.repo_name,
1822 'repo_type': repo.repo_type,
1822 'repo_type': repo.repo_type,
1823 'clone_uri': repo.clone_uri or '',
1823 'clone_uri': repo.clone_uri or '',
1824 'url': RepoModel().get_url(self),
1824 'url': RepoModel().get_url(self),
1825 'private': repo.private,
1825 'private': repo.private,
1826 'created_on': repo.created_on,
1826 'created_on': repo.created_on,
1827 'description': repo.description_safe,
1827 'description': repo.description_safe,
1828 'landing_rev': repo.landing_rev,
1828 'landing_rev': repo.landing_rev,
1829 'owner': repo.user.username,
1829 'owner': repo.user.username,
1830 'fork_of': repo.fork.repo_name if repo.fork else None,
1830 'fork_of': repo.fork.repo_name if repo.fork else None,
1831 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1831 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1832 'enable_statistics': repo.enable_statistics,
1832 'enable_statistics': repo.enable_statistics,
1833 'enable_locking': repo.enable_locking,
1833 'enable_locking': repo.enable_locking,
1834 'enable_downloads': repo.enable_downloads,
1834 'enable_downloads': repo.enable_downloads,
1835 'last_changeset': repo.changeset_cache,
1835 'last_changeset': repo.changeset_cache,
1836 'locked_by': User.get(_user_id).get_api_data(
1836 'locked_by': User.get(_user_id).get_api_data(
1837 include_secrets=include_secrets) if _user_id else None,
1837 include_secrets=include_secrets) if _user_id else None,
1838 'locked_date': time_to_datetime(_time) if _time else None,
1838 'locked_date': time_to_datetime(_time) if _time else None,
1839 'lock_reason': _reason if _reason else None,
1839 'lock_reason': _reason if _reason else None,
1840 }
1840 }
1841
1841
1842 # TODO: mikhail: should be per-repo settings here
1842 # TODO: mikhail: should be per-repo settings here
1843 rc_config = SettingsModel().get_all_settings()
1843 rc_config = SettingsModel().get_all_settings()
1844 repository_fields = str2bool(
1844 repository_fields = str2bool(
1845 rc_config.get('rhodecode_repository_fields'))
1845 rc_config.get('rhodecode_repository_fields'))
1846 if repository_fields:
1846 if repository_fields:
1847 for f in self.extra_fields:
1847 for f in self.extra_fields:
1848 data[f.field_key_prefixed] = f.field_value
1848 data[f.field_key_prefixed] = f.field_value
1849
1849
1850 return data
1850 return data
1851
1851
1852 @classmethod
1852 @classmethod
1853 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1853 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1854 if not lock_time:
1854 if not lock_time:
1855 lock_time = time.time()
1855 lock_time = time.time()
1856 if not lock_reason:
1856 if not lock_reason:
1857 lock_reason = cls.LOCK_AUTOMATIC
1857 lock_reason = cls.LOCK_AUTOMATIC
1858 repo.locked = [user_id, lock_time, lock_reason]
1858 repo.locked = [user_id, lock_time, lock_reason]
1859 Session().add(repo)
1859 Session().add(repo)
1860 Session().commit()
1860 Session().commit()
1861
1861
1862 @classmethod
1862 @classmethod
1863 def unlock(cls, repo):
1863 def unlock(cls, repo):
1864 repo.locked = None
1864 repo.locked = None
1865 Session().add(repo)
1865 Session().add(repo)
1866 Session().commit()
1866 Session().commit()
1867
1867
1868 @classmethod
1868 @classmethod
1869 def getlock(cls, repo):
1869 def getlock(cls, repo):
1870 return repo.locked
1870 return repo.locked
1871
1871
1872 def is_user_lock(self, user_id):
1872 def is_user_lock(self, user_id):
1873 if self.lock[0]:
1873 if self.lock[0]:
1874 lock_user_id = safe_int(self.lock[0])
1874 lock_user_id = safe_int(self.lock[0])
1875 user_id = safe_int(user_id)
1875 user_id = safe_int(user_id)
1876 # both are ints, and they are equal
1876 # both are ints, and they are equal
1877 return all([lock_user_id, user_id]) and lock_user_id == user_id
1877 return all([lock_user_id, user_id]) and lock_user_id == user_id
1878
1878
1879 return False
1879 return False
1880
1880
1881 def get_locking_state(self, action, user_id, only_when_enabled=True):
1881 def get_locking_state(self, action, user_id, only_when_enabled=True):
1882 """
1882 """
1883 Checks locking on this repository, if locking is enabled and lock is
1883 Checks locking on this repository, if locking is enabled and lock is
1884 present returns a tuple of make_lock, locked, locked_by.
1884 present returns a tuple of make_lock, locked, locked_by.
1885 make_lock can have 3 states None (do nothing) True, make lock
1885 make_lock can have 3 states None (do nothing) True, make lock
1886 False release lock, This value is later propagated to hooks, which
1886 False release lock, This value is later propagated to hooks, which
1887 do the locking. Think about this as signals passed to hooks what to do.
1887 do the locking. Think about this as signals passed to hooks what to do.
1888
1888
1889 """
1889 """
1890 # TODO: johbo: This is part of the business logic and should be moved
1890 # TODO: johbo: This is part of the business logic and should be moved
1891 # into the RepositoryModel.
1891 # into the RepositoryModel.
1892
1892
1893 if action not in ('push', 'pull'):
1893 if action not in ('push', 'pull'):
1894 raise ValueError("Invalid action value: %s" % repr(action))
1894 raise ValueError("Invalid action value: %s" % repr(action))
1895
1895
1896 # defines if locked error should be thrown to user
1896 # defines if locked error should be thrown to user
1897 currently_locked = False
1897 currently_locked = False
1898 # defines if new lock should be made, tri-state
1898 # defines if new lock should be made, tri-state
1899 make_lock = None
1899 make_lock = None
1900 repo = self
1900 repo = self
1901 user = User.get(user_id)
1901 user = User.get(user_id)
1902
1902
1903 lock_info = repo.locked
1903 lock_info = repo.locked
1904
1904
1905 if repo and (repo.enable_locking or not only_when_enabled):
1905 if repo and (repo.enable_locking or not only_when_enabled):
1906 if action == 'push':
1906 if action == 'push':
1907 # check if it's already locked !, if it is compare users
1907 # check if it's already locked !, if it is compare users
1908 locked_by_user_id = lock_info[0]
1908 locked_by_user_id = lock_info[0]
1909 if user.user_id == locked_by_user_id:
1909 if user.user_id == locked_by_user_id:
1910 log.debug(
1910 log.debug(
1911 'Got `push` action from user %s, now unlocking', user)
1911 'Got `push` action from user %s, now unlocking', user)
1912 # unlock if we have push from user who locked
1912 # unlock if we have push from user who locked
1913 make_lock = False
1913 make_lock = False
1914 else:
1914 else:
1915 # we're not the same user who locked, ban with
1915 # we're not the same user who locked, ban with
1916 # code defined in settings (default is 423 HTTP Locked) !
1916 # code defined in settings (default is 423 HTTP Locked) !
1917 log.debug('Repo %s is currently locked by %s', repo, user)
1917 log.debug('Repo %s is currently locked by %s', repo, user)
1918 currently_locked = True
1918 currently_locked = True
1919 elif action == 'pull':
1919 elif action == 'pull':
1920 # [0] user [1] date
1920 # [0] user [1] date
1921 if lock_info[0] and lock_info[1]:
1921 if lock_info[0] and lock_info[1]:
1922 log.debug('Repo %s is currently locked by %s', repo, user)
1922 log.debug('Repo %s is currently locked by %s', repo, user)
1923 currently_locked = True
1923 currently_locked = True
1924 else:
1924 else:
1925 log.debug('Setting lock on repo %s by %s', repo, user)
1925 log.debug('Setting lock on repo %s by %s', repo, user)
1926 make_lock = True
1926 make_lock = True
1927
1927
1928 else:
1928 else:
1929 log.debug('Repository %s do not have locking enabled', repo)
1929 log.debug('Repository %s do not have locking enabled', repo)
1930
1930
1931 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1931 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1932 make_lock, currently_locked, lock_info)
1932 make_lock, currently_locked, lock_info)
1933
1933
1934 from rhodecode.lib.auth import HasRepoPermissionAny
1934 from rhodecode.lib.auth import HasRepoPermissionAny
1935 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1935 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1936 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1936 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1937 # if we don't have at least write permission we cannot make a lock
1937 # if we don't have at least write permission we cannot make a lock
1938 log.debug('lock state reset back to FALSE due to lack '
1938 log.debug('lock state reset back to FALSE due to lack '
1939 'of at least read permission')
1939 'of at least read permission')
1940 make_lock = False
1940 make_lock = False
1941
1941
1942 return make_lock, currently_locked, lock_info
1942 return make_lock, currently_locked, lock_info
1943
1943
1944 @property
1944 @property
1945 def last_db_change(self):
1945 def last_db_change(self):
1946 return self.updated_on
1946 return self.updated_on
1947
1947
1948 @property
1948 @property
1949 def clone_uri_hidden(self):
1949 def clone_uri_hidden(self):
1950 clone_uri = self.clone_uri
1950 clone_uri = self.clone_uri
1951 if clone_uri:
1951 if clone_uri:
1952 import urlobject
1952 import urlobject
1953 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1953 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1954 if url_obj.password:
1954 if url_obj.password:
1955 clone_uri = url_obj.with_password('*****')
1955 clone_uri = url_obj.with_password('*****')
1956 return clone_uri
1956 return clone_uri
1957
1957
1958 def clone_url(self, **override):
1958 def clone_url(self, **override):
1959 from rhodecode.model.settings import SettingsModel
1959 from rhodecode.model.settings import SettingsModel
1960
1960
1961 uri_tmpl = None
1961 uri_tmpl = None
1962 if 'with_id' in override:
1962 if 'with_id' in override:
1963 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1963 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1964 del override['with_id']
1964 del override['with_id']
1965
1965
1966 if 'uri_tmpl' in override:
1966 if 'uri_tmpl' in override:
1967 uri_tmpl = override['uri_tmpl']
1967 uri_tmpl = override['uri_tmpl']
1968 del override['uri_tmpl']
1968 del override['uri_tmpl']
1969
1969
1970 # we didn't override our tmpl from **overrides
1970 # we didn't override our tmpl from **overrides
1971 if not uri_tmpl:
1971 if not uri_tmpl:
1972 rc_config = SettingsModel().get_all_settings(cache=True)
1972 rc_config = SettingsModel().get_all_settings(cache=True)
1973 uri_tmpl = rc_config.get(
1973 uri_tmpl = rc_config.get(
1974 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
1974 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
1975
1975
1976 request = get_current_request()
1976 request = get_current_request()
1977 return get_clone_url(request=request,
1977 return get_clone_url(request=request,
1978 uri_tmpl=uri_tmpl,
1978 uri_tmpl=uri_tmpl,
1979 repo_name=self.repo_name,
1979 repo_name=self.repo_name,
1980 repo_id=self.repo_id, **override)
1980 repo_id=self.repo_id, **override)
1981
1981
1982 def set_state(self, state):
1982 def set_state(self, state):
1983 self.repo_state = state
1983 self.repo_state = state
1984 Session().add(self)
1984 Session().add(self)
1985 #==========================================================================
1985 #==========================================================================
1986 # SCM PROPERTIES
1986 # SCM PROPERTIES
1987 #==========================================================================
1987 #==========================================================================
1988
1988
1989 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1989 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1990 return get_commit_safe(
1990 return get_commit_safe(
1991 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1991 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1992
1992
1993 def get_changeset(self, rev=None, pre_load=None):
1993 def get_changeset(self, rev=None, pre_load=None):
1994 warnings.warn("Use get_commit", DeprecationWarning)
1994 warnings.warn("Use get_commit", DeprecationWarning)
1995 commit_id = None
1995 commit_id = None
1996 commit_idx = None
1996 commit_idx = None
1997 if isinstance(rev, basestring):
1997 if isinstance(rev, basestring):
1998 commit_id = rev
1998 commit_id = rev
1999 else:
1999 else:
2000 commit_idx = rev
2000 commit_idx = rev
2001 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2001 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2002 pre_load=pre_load)
2002 pre_load=pre_load)
2003
2003
2004 def get_landing_commit(self):
2004 def get_landing_commit(self):
2005 """
2005 """
2006 Returns landing commit, or if that doesn't exist returns the tip
2006 Returns landing commit, or if that doesn't exist returns the tip
2007 """
2007 """
2008 _rev_type, _rev = self.landing_rev
2008 _rev_type, _rev = self.landing_rev
2009 commit = self.get_commit(_rev)
2009 commit = self.get_commit(_rev)
2010 if isinstance(commit, EmptyCommit):
2010 if isinstance(commit, EmptyCommit):
2011 return self.get_commit()
2011 return self.get_commit()
2012 return commit
2012 return commit
2013
2013
2014 def update_commit_cache(self, cs_cache=None, config=None):
2014 def update_commit_cache(self, cs_cache=None, config=None):
2015 """
2015 """
2016 Update cache of last changeset for repository, keys should be::
2016 Update cache of last changeset for repository, keys should be::
2017
2017
2018 short_id
2018 short_id
2019 raw_id
2019 raw_id
2020 revision
2020 revision
2021 parents
2021 parents
2022 message
2022 message
2023 date
2023 date
2024 author
2024 author
2025
2025
2026 :param cs_cache:
2026 :param cs_cache:
2027 """
2027 """
2028 from rhodecode.lib.vcs.backends.base import BaseChangeset
2028 from rhodecode.lib.vcs.backends.base import BaseChangeset
2029 if cs_cache is None:
2029 if cs_cache is None:
2030 # use no-cache version here
2030 # use no-cache version here
2031 scm_repo = self.scm_instance(cache=False, config=config)
2031 scm_repo = self.scm_instance(cache=False, config=config)
2032 if scm_repo:
2032 if scm_repo:
2033 cs_cache = scm_repo.get_commit(
2033 cs_cache = scm_repo.get_commit(
2034 pre_load=["author", "date", "message", "parents"])
2034 pre_load=["author", "date", "message", "parents"])
2035 else:
2035 else:
2036 cs_cache = EmptyCommit()
2036 cs_cache = EmptyCommit()
2037
2037
2038 if isinstance(cs_cache, BaseChangeset):
2038 if isinstance(cs_cache, BaseChangeset):
2039 cs_cache = cs_cache.__json__()
2039 cs_cache = cs_cache.__json__()
2040
2040
2041 def is_outdated(new_cs_cache):
2041 def is_outdated(new_cs_cache):
2042 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2042 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2043 new_cs_cache['revision'] != self.changeset_cache['revision']):
2043 new_cs_cache['revision'] != self.changeset_cache['revision']):
2044 return True
2044 return True
2045 return False
2045 return False
2046
2046
2047 # check if we have maybe already latest cached revision
2047 # check if we have maybe already latest cached revision
2048 if is_outdated(cs_cache) or not self.changeset_cache:
2048 if is_outdated(cs_cache) or not self.changeset_cache:
2049 _default = datetime.datetime.fromtimestamp(0)
2049 _default = datetime.datetime.fromtimestamp(0)
2050 last_change = cs_cache.get('date') or _default
2050 last_change = cs_cache.get('date') or _default
2051 log.debug('updated repo %s with new cs cache %s',
2051 log.debug('updated repo %s with new cs cache %s',
2052 self.repo_name, cs_cache)
2052 self.repo_name, cs_cache)
2053 self.updated_on = last_change
2053 self.updated_on = last_change
2054 self.changeset_cache = cs_cache
2054 self.changeset_cache = cs_cache
2055 Session().add(self)
2055 Session().add(self)
2056 Session().commit()
2056 Session().commit()
2057 else:
2057 else:
2058 log.debug('Skipping update_commit_cache for repo:`%s` '
2058 log.debug('Skipping update_commit_cache for repo:`%s` '
2059 'commit already with latest changes', self.repo_name)
2059 'commit already with latest changes', self.repo_name)
2060
2060
2061 @property
2061 @property
2062 def tip(self):
2062 def tip(self):
2063 return self.get_commit('tip')
2063 return self.get_commit('tip')
2064
2064
2065 @property
2065 @property
2066 def author(self):
2066 def author(self):
2067 return self.tip.author
2067 return self.tip.author
2068
2068
2069 @property
2069 @property
2070 def last_change(self):
2070 def last_change(self):
2071 return self.scm_instance().last_change
2071 return self.scm_instance().last_change
2072
2072
2073 def get_comments(self, revisions=None):
2073 def get_comments(self, revisions=None):
2074 """
2074 """
2075 Returns comments for this repository grouped by revisions
2075 Returns comments for this repository grouped by revisions
2076
2076
2077 :param revisions: filter query by revisions only
2077 :param revisions: filter query by revisions only
2078 """
2078 """
2079 cmts = ChangesetComment.query()\
2079 cmts = ChangesetComment.query()\
2080 .filter(ChangesetComment.repo == self)
2080 .filter(ChangesetComment.repo == self)
2081 if revisions:
2081 if revisions:
2082 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2082 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2083 grouped = collections.defaultdict(list)
2083 grouped = collections.defaultdict(list)
2084 for cmt in cmts.all():
2084 for cmt in cmts.all():
2085 grouped[cmt.revision].append(cmt)
2085 grouped[cmt.revision].append(cmt)
2086 return grouped
2086 return grouped
2087
2087
2088 def statuses(self, revisions=None):
2088 def statuses(self, revisions=None):
2089 """
2089 """
2090 Returns statuses for this repository
2090 Returns statuses for this repository
2091
2091
2092 :param revisions: list of revisions to get statuses for
2092 :param revisions: list of revisions to get statuses for
2093 """
2093 """
2094 statuses = ChangesetStatus.query()\
2094 statuses = ChangesetStatus.query()\
2095 .filter(ChangesetStatus.repo == self)\
2095 .filter(ChangesetStatus.repo == self)\
2096 .filter(ChangesetStatus.version == 0)
2096 .filter(ChangesetStatus.version == 0)
2097
2097
2098 if revisions:
2098 if revisions:
2099 # Try doing the filtering in chunks to avoid hitting limits
2099 # Try doing the filtering in chunks to avoid hitting limits
2100 size = 500
2100 size = 500
2101 status_results = []
2101 status_results = []
2102 for chunk in xrange(0, len(revisions), size):
2102 for chunk in xrange(0, len(revisions), size):
2103 status_results += statuses.filter(
2103 status_results += statuses.filter(
2104 ChangesetStatus.revision.in_(
2104 ChangesetStatus.revision.in_(
2105 revisions[chunk: chunk+size])
2105 revisions[chunk: chunk+size])
2106 ).all()
2106 ).all()
2107 else:
2107 else:
2108 status_results = statuses.all()
2108 status_results = statuses.all()
2109
2109
2110 grouped = {}
2110 grouped = {}
2111
2111
2112 # maybe we have open new pullrequest without a status?
2112 # maybe we have open new pullrequest without a status?
2113 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2113 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2114 status_lbl = ChangesetStatus.get_status_lbl(stat)
2114 status_lbl = ChangesetStatus.get_status_lbl(stat)
2115 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2115 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2116 for rev in pr.revisions:
2116 for rev in pr.revisions:
2117 pr_id = pr.pull_request_id
2117 pr_id = pr.pull_request_id
2118 pr_repo = pr.target_repo.repo_name
2118 pr_repo = pr.target_repo.repo_name
2119 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2119 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2120
2120
2121 for stat in status_results:
2121 for stat in status_results:
2122 pr_id = pr_repo = None
2122 pr_id = pr_repo = None
2123 if stat.pull_request:
2123 if stat.pull_request:
2124 pr_id = stat.pull_request.pull_request_id
2124 pr_id = stat.pull_request.pull_request_id
2125 pr_repo = stat.pull_request.target_repo.repo_name
2125 pr_repo = stat.pull_request.target_repo.repo_name
2126 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2126 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2127 pr_id, pr_repo]
2127 pr_id, pr_repo]
2128 return grouped
2128 return grouped
2129
2129
2130 # ==========================================================================
2130 # ==========================================================================
2131 # SCM CACHE INSTANCE
2131 # SCM CACHE INSTANCE
2132 # ==========================================================================
2132 # ==========================================================================
2133
2133
2134 def scm_instance(self, **kwargs):
2134 def scm_instance(self, **kwargs):
2135 import rhodecode
2135 import rhodecode
2136
2136
2137 # Passing a config will not hit the cache currently only used
2137 # Passing a config will not hit the cache currently only used
2138 # for repo2dbmapper
2138 # for repo2dbmapper
2139 config = kwargs.pop('config', None)
2139 config = kwargs.pop('config', None)
2140 cache = kwargs.pop('cache', None)
2140 cache = kwargs.pop('cache', None)
2141 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2141 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2142 # if cache is NOT defined use default global, else we have a full
2142 # if cache is NOT defined use default global, else we have a full
2143 # control over cache behaviour
2143 # control over cache behaviour
2144 if cache is None and full_cache and not config:
2144 if cache is None and full_cache and not config:
2145 return self._get_instance_cached()
2145 return self._get_instance_cached()
2146 return self._get_instance(cache=bool(cache), config=config)
2146 return self._get_instance(cache=bool(cache), config=config)
2147
2147
2148 def _get_instance_cached(self):
2148 def _get_instance_cached(self):
2149 @cache_region('long_term')
2149 @cache_region('long_term')
2150 def _get_repo(cache_key):
2150 def _get_repo(cache_key):
2151 return self._get_instance()
2151 return self._get_instance()
2152
2152
2153 invalidator_context = CacheKey.repo_context_cache(
2153 invalidator_context = CacheKey.repo_context_cache(
2154 _get_repo, self.repo_name, None, thread_scoped=True)
2154 _get_repo, self.repo_name, None, thread_scoped=True)
2155
2155
2156 with invalidator_context as context:
2156 with invalidator_context as context:
2157 context.invalidate()
2157 context.invalidate()
2158 repo = context.compute()
2158 repo = context.compute()
2159
2159
2160 return repo
2160 return repo
2161
2161
2162 def _get_instance(self, cache=True, config=None):
2162 def _get_instance(self, cache=True, config=None):
2163 config = config or self._config
2163 config = config or self._config
2164 custom_wire = {
2164 custom_wire = {
2165 'cache': cache # controls the vcs.remote cache
2165 'cache': cache # controls the vcs.remote cache
2166 }
2166 }
2167 repo = get_vcs_instance(
2167 repo = get_vcs_instance(
2168 repo_path=safe_str(self.repo_full_path),
2168 repo_path=safe_str(self.repo_full_path),
2169 config=config,
2169 config=config,
2170 with_wire=custom_wire,
2170 with_wire=custom_wire,
2171 create=False,
2171 create=False,
2172 _vcs_alias=self.repo_type)
2172 _vcs_alias=self.repo_type)
2173
2173
2174 return repo
2174 return repo
2175
2175
2176 def __json__(self):
2176 def __json__(self):
2177 return {'landing_rev': self.landing_rev}
2177 return {'landing_rev': self.landing_rev}
2178
2178
2179 def get_dict(self):
2179 def get_dict(self):
2180
2180
2181 # Since we transformed `repo_name` to a hybrid property, we need to
2181 # Since we transformed `repo_name` to a hybrid property, we need to
2182 # keep compatibility with the code which uses `repo_name` field.
2182 # keep compatibility with the code which uses `repo_name` field.
2183
2183
2184 result = super(Repository, self).get_dict()
2184 result = super(Repository, self).get_dict()
2185 result['repo_name'] = result.pop('_repo_name', None)
2185 result['repo_name'] = result.pop('_repo_name', None)
2186 return result
2186 return result
2187
2187
2188
2188
2189 class RepoGroup(Base, BaseModel):
2189 class RepoGroup(Base, BaseModel):
2190 __tablename__ = 'groups'
2190 __tablename__ = 'groups'
2191 __table_args__ = (
2191 __table_args__ = (
2192 UniqueConstraint('group_name', 'group_parent_id'),
2192 UniqueConstraint('group_name', 'group_parent_id'),
2193 CheckConstraint('group_id != group_parent_id'),
2193 CheckConstraint('group_id != group_parent_id'),
2194 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2194 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2195 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2195 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2196 )
2196 )
2197 __mapper_args__ = {'order_by': 'group_name'}
2197 __mapper_args__ = {'order_by': 'group_name'}
2198
2198
2199 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2199 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2200
2200
2201 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2201 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2202 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2202 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2203 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2203 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2204 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2204 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2205 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2205 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2206 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2206 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2207 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2207 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2208 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2208 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2209 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2209 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2210
2210
2211 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2211 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2212 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2212 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2213 parent_group = relationship('RepoGroup', remote_side=group_id)
2213 parent_group = relationship('RepoGroup', remote_side=group_id)
2214 user = relationship('User')
2214 user = relationship('User')
2215 integrations = relationship('Integration',
2215 integrations = relationship('Integration',
2216 cascade="all, delete, delete-orphan")
2216 cascade="all, delete, delete-orphan")
2217
2217
2218 def __init__(self, group_name='', parent_group=None):
2218 def __init__(self, group_name='', parent_group=None):
2219 self.group_name = group_name
2219 self.group_name = group_name
2220 self.parent_group = parent_group
2220 self.parent_group = parent_group
2221
2221
2222 def __unicode__(self):
2222 def __unicode__(self):
2223 return u"<%s('id:%s:%s')>" % (
2223 return u"<%s('id:%s:%s')>" % (
2224 self.__class__.__name__, self.group_id, self.group_name)
2224 self.__class__.__name__, self.group_id, self.group_name)
2225
2225
2226 @hybrid_property
2226 @hybrid_property
2227 def description_safe(self):
2227 def description_safe(self):
2228 from rhodecode.lib import helpers as h
2228 from rhodecode.lib import helpers as h
2229 return h.escape(self.group_description)
2229 return h.escape(self.group_description)
2230
2230
2231 @classmethod
2231 @classmethod
2232 def _generate_choice(cls, repo_group):
2232 def _generate_choice(cls, repo_group):
2233 from webhelpers.html import literal as _literal
2233 from webhelpers.html import literal as _literal
2234 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2234 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2235 return repo_group.group_id, _name(repo_group.full_path_splitted)
2235 return repo_group.group_id, _name(repo_group.full_path_splitted)
2236
2236
2237 @classmethod
2237 @classmethod
2238 def groups_choices(cls, groups=None, show_empty_group=True):
2238 def groups_choices(cls, groups=None, show_empty_group=True):
2239 if not groups:
2239 if not groups:
2240 groups = cls.query().all()
2240 groups = cls.query().all()
2241
2241
2242 repo_groups = []
2242 repo_groups = []
2243 if show_empty_group:
2243 if show_empty_group:
2244 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2244 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2245
2245
2246 repo_groups.extend([cls._generate_choice(x) for x in groups])
2246 repo_groups.extend([cls._generate_choice(x) for x in groups])
2247
2247
2248 repo_groups = sorted(
2248 repo_groups = sorted(
2249 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2249 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2250 return repo_groups
2250 return repo_groups
2251
2251
2252 @classmethod
2252 @classmethod
2253 def url_sep(cls):
2253 def url_sep(cls):
2254 return URL_SEP
2254 return URL_SEP
2255
2255
2256 @classmethod
2256 @classmethod
2257 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2257 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2258 if case_insensitive:
2258 if case_insensitive:
2259 gr = cls.query().filter(func.lower(cls.group_name)
2259 gr = cls.query().filter(func.lower(cls.group_name)
2260 == func.lower(group_name))
2260 == func.lower(group_name))
2261 else:
2261 else:
2262 gr = cls.query().filter(cls.group_name == group_name)
2262 gr = cls.query().filter(cls.group_name == group_name)
2263 if cache:
2263 if cache:
2264 name_key = _hash_key(group_name)
2264 name_key = _hash_key(group_name)
2265 gr = gr.options(
2265 gr = gr.options(
2266 FromCache("sql_cache_short", "get_group_%s" % name_key))
2266 FromCache("sql_cache_short", "get_group_%s" % name_key))
2267 return gr.scalar()
2267 return gr.scalar()
2268
2268
2269 @classmethod
2269 @classmethod
2270 def get_user_personal_repo_group(cls, user_id):
2270 def get_user_personal_repo_group(cls, user_id):
2271 user = User.get(user_id)
2271 user = User.get(user_id)
2272 if user.username == User.DEFAULT_USER:
2272 if user.username == User.DEFAULT_USER:
2273 return None
2273 return None
2274
2274
2275 return cls.query()\
2275 return cls.query()\
2276 .filter(cls.personal == true()) \
2276 .filter(cls.personal == true()) \
2277 .filter(cls.user == user).scalar()
2277 .filter(cls.user == user).scalar()
2278
2278
2279 @classmethod
2279 @classmethod
2280 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2280 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2281 case_insensitive=True):
2281 case_insensitive=True):
2282 q = RepoGroup.query()
2282 q = RepoGroup.query()
2283
2283
2284 if not isinstance(user_id, Optional):
2284 if not isinstance(user_id, Optional):
2285 q = q.filter(RepoGroup.user_id == user_id)
2285 q = q.filter(RepoGroup.user_id == user_id)
2286
2286
2287 if not isinstance(group_id, Optional):
2287 if not isinstance(group_id, Optional):
2288 q = q.filter(RepoGroup.group_parent_id == group_id)
2288 q = q.filter(RepoGroup.group_parent_id == group_id)
2289
2289
2290 if case_insensitive:
2290 if case_insensitive:
2291 q = q.order_by(func.lower(RepoGroup.group_name))
2291 q = q.order_by(func.lower(RepoGroup.group_name))
2292 else:
2292 else:
2293 q = q.order_by(RepoGroup.group_name)
2293 q = q.order_by(RepoGroup.group_name)
2294 return q.all()
2294 return q.all()
2295
2295
2296 @property
2296 @property
2297 def parents(self):
2297 def parents(self):
2298 parents_recursion_limit = 10
2298 parents_recursion_limit = 10
2299 groups = []
2299 groups = []
2300 if self.parent_group is None:
2300 if self.parent_group is None:
2301 return groups
2301 return groups
2302 cur_gr = self.parent_group
2302 cur_gr = self.parent_group
2303 groups.insert(0, cur_gr)
2303 groups.insert(0, cur_gr)
2304 cnt = 0
2304 cnt = 0
2305 while 1:
2305 while 1:
2306 cnt += 1
2306 cnt += 1
2307 gr = getattr(cur_gr, 'parent_group', None)
2307 gr = getattr(cur_gr, 'parent_group', None)
2308 cur_gr = cur_gr.parent_group
2308 cur_gr = cur_gr.parent_group
2309 if gr is None:
2309 if gr is None:
2310 break
2310 break
2311 if cnt == parents_recursion_limit:
2311 if cnt == parents_recursion_limit:
2312 # this will prevent accidental infinit loops
2312 # this will prevent accidental infinit loops
2313 log.error(('more than %s parents found for group %s, stopping '
2313 log.error(('more than %s parents found for group %s, stopping '
2314 'recursive parent fetching' % (parents_recursion_limit, self)))
2314 'recursive parent fetching' % (parents_recursion_limit, self)))
2315 break
2315 break
2316
2316
2317 groups.insert(0, gr)
2317 groups.insert(0, gr)
2318 return groups
2318 return groups
2319
2319
2320 @property
2320 @property
2321 def last_db_change(self):
2321 def last_db_change(self):
2322 return self.updated_on
2322 return self.updated_on
2323
2323
2324 @property
2324 @property
2325 def children(self):
2325 def children(self):
2326 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2326 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2327
2327
2328 @property
2328 @property
2329 def name(self):
2329 def name(self):
2330 return self.group_name.split(RepoGroup.url_sep())[-1]
2330 return self.group_name.split(RepoGroup.url_sep())[-1]
2331
2331
2332 @property
2332 @property
2333 def full_path(self):
2333 def full_path(self):
2334 return self.group_name
2334 return self.group_name
2335
2335
2336 @property
2336 @property
2337 def full_path_splitted(self):
2337 def full_path_splitted(self):
2338 return self.group_name.split(RepoGroup.url_sep())
2338 return self.group_name.split(RepoGroup.url_sep())
2339
2339
2340 @property
2340 @property
2341 def repositories(self):
2341 def repositories(self):
2342 return Repository.query()\
2342 return Repository.query()\
2343 .filter(Repository.group == self)\
2343 .filter(Repository.group == self)\
2344 .order_by(Repository.repo_name)
2344 .order_by(Repository.repo_name)
2345
2345
2346 @property
2346 @property
2347 def repositories_recursive_count(self):
2347 def repositories_recursive_count(self):
2348 cnt = self.repositories.count()
2348 cnt = self.repositories.count()
2349
2349
2350 def children_count(group):
2350 def children_count(group):
2351 cnt = 0
2351 cnt = 0
2352 for child in group.children:
2352 for child in group.children:
2353 cnt += child.repositories.count()
2353 cnt += child.repositories.count()
2354 cnt += children_count(child)
2354 cnt += children_count(child)
2355 return cnt
2355 return cnt
2356
2356
2357 return cnt + children_count(self)
2357 return cnt + children_count(self)
2358
2358
2359 def _recursive_objects(self, include_repos=True):
2359 def _recursive_objects(self, include_repos=True):
2360 all_ = []
2360 all_ = []
2361
2361
2362 def _get_members(root_gr):
2362 def _get_members(root_gr):
2363 if include_repos:
2363 if include_repos:
2364 for r in root_gr.repositories:
2364 for r in root_gr.repositories:
2365 all_.append(r)
2365 all_.append(r)
2366 childs = root_gr.children.all()
2366 childs = root_gr.children.all()
2367 if childs:
2367 if childs:
2368 for gr in childs:
2368 for gr in childs:
2369 all_.append(gr)
2369 all_.append(gr)
2370 _get_members(gr)
2370 _get_members(gr)
2371
2371
2372 _get_members(self)
2372 _get_members(self)
2373 return [self] + all_
2373 return [self] + all_
2374
2374
2375 def recursive_groups_and_repos(self):
2375 def recursive_groups_and_repos(self):
2376 """
2376 """
2377 Recursive return all groups, with repositories in those groups
2377 Recursive return all groups, with repositories in those groups
2378 """
2378 """
2379 return self._recursive_objects()
2379 return self._recursive_objects()
2380
2380
2381 def recursive_groups(self):
2381 def recursive_groups(self):
2382 """
2382 """
2383 Returns all children groups for this group including children of children
2383 Returns all children groups for this group including children of children
2384 """
2384 """
2385 return self._recursive_objects(include_repos=False)
2385 return self._recursive_objects(include_repos=False)
2386
2386
2387 def get_new_name(self, group_name):
2387 def get_new_name(self, group_name):
2388 """
2388 """
2389 returns new full group name based on parent and new name
2389 returns new full group name based on parent and new name
2390
2390
2391 :param group_name:
2391 :param group_name:
2392 """
2392 """
2393 path_prefix = (self.parent_group.full_path_splitted if
2393 path_prefix = (self.parent_group.full_path_splitted if
2394 self.parent_group else [])
2394 self.parent_group else [])
2395 return RepoGroup.url_sep().join(path_prefix + [group_name])
2395 return RepoGroup.url_sep().join(path_prefix + [group_name])
2396
2396
2397 def permissions(self, with_admins=True, with_owner=True):
2397 def permissions(self, with_admins=True, with_owner=True):
2398 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2398 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2399 q = q.options(joinedload(UserRepoGroupToPerm.group),
2399 q = q.options(joinedload(UserRepoGroupToPerm.group),
2400 joinedload(UserRepoGroupToPerm.user),
2400 joinedload(UserRepoGroupToPerm.user),
2401 joinedload(UserRepoGroupToPerm.permission),)
2401 joinedload(UserRepoGroupToPerm.permission),)
2402
2402
2403 # get owners and admins and permissions. We do a trick of re-writing
2403 # get owners and admins and permissions. We do a trick of re-writing
2404 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2404 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2405 # has a global reference and changing one object propagates to all
2405 # has a global reference and changing one object propagates to all
2406 # others. This means if admin is also an owner admin_row that change
2406 # others. This means if admin is also an owner admin_row that change
2407 # would propagate to both objects
2407 # would propagate to both objects
2408 perm_rows = []
2408 perm_rows = []
2409 for _usr in q.all():
2409 for _usr in q.all():
2410 usr = AttributeDict(_usr.user.get_dict())
2410 usr = AttributeDict(_usr.user.get_dict())
2411 usr.permission = _usr.permission.permission_name
2411 usr.permission = _usr.permission.permission_name
2412 perm_rows.append(usr)
2412 perm_rows.append(usr)
2413
2413
2414 # filter the perm rows by 'default' first and then sort them by
2414 # filter the perm rows by 'default' first and then sort them by
2415 # admin,write,read,none permissions sorted again alphabetically in
2415 # admin,write,read,none permissions sorted again alphabetically in
2416 # each group
2416 # each group
2417 perm_rows = sorted(perm_rows, key=display_sort)
2417 perm_rows = sorted(perm_rows, key=display_sort)
2418
2418
2419 _admin_perm = 'group.admin'
2419 _admin_perm = 'group.admin'
2420 owner_row = []
2420 owner_row = []
2421 if with_owner:
2421 if with_owner:
2422 usr = AttributeDict(self.user.get_dict())
2422 usr = AttributeDict(self.user.get_dict())
2423 usr.owner_row = True
2423 usr.owner_row = True
2424 usr.permission = _admin_perm
2424 usr.permission = _admin_perm
2425 owner_row.append(usr)
2425 owner_row.append(usr)
2426
2426
2427 super_admin_rows = []
2427 super_admin_rows = []
2428 if with_admins:
2428 if with_admins:
2429 for usr in User.get_all_super_admins():
2429 for usr in User.get_all_super_admins():
2430 # if this admin is also owner, don't double the record
2430 # if this admin is also owner, don't double the record
2431 if usr.user_id == owner_row[0].user_id:
2431 if usr.user_id == owner_row[0].user_id:
2432 owner_row[0].admin_row = True
2432 owner_row[0].admin_row = True
2433 else:
2433 else:
2434 usr = AttributeDict(usr.get_dict())
2434 usr = AttributeDict(usr.get_dict())
2435 usr.admin_row = True
2435 usr.admin_row = True
2436 usr.permission = _admin_perm
2436 usr.permission = _admin_perm
2437 super_admin_rows.append(usr)
2437 super_admin_rows.append(usr)
2438
2438
2439 return super_admin_rows + owner_row + perm_rows
2439 return super_admin_rows + owner_row + perm_rows
2440
2440
2441 def permission_user_groups(self):
2441 def permission_user_groups(self):
2442 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2442 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2443 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2443 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2444 joinedload(UserGroupRepoGroupToPerm.users_group),
2444 joinedload(UserGroupRepoGroupToPerm.users_group),
2445 joinedload(UserGroupRepoGroupToPerm.permission),)
2445 joinedload(UserGroupRepoGroupToPerm.permission),)
2446
2446
2447 perm_rows = []
2447 perm_rows = []
2448 for _user_group in q.all():
2448 for _user_group in q.all():
2449 usr = AttributeDict(_user_group.users_group.get_dict())
2449 usr = AttributeDict(_user_group.users_group.get_dict())
2450 usr.permission = _user_group.permission.permission_name
2450 usr.permission = _user_group.permission.permission_name
2451 perm_rows.append(usr)
2451 perm_rows.append(usr)
2452
2452
2453 return perm_rows
2453 return perm_rows
2454
2454
2455 def get_api_data(self):
2455 def get_api_data(self):
2456 """
2456 """
2457 Common function for generating api data
2457 Common function for generating api data
2458
2458
2459 """
2459 """
2460 group = self
2460 group = self
2461 data = {
2461 data = {
2462 'group_id': group.group_id,
2462 'group_id': group.group_id,
2463 'group_name': group.group_name,
2463 'group_name': group.group_name,
2464 'group_description': group.description_safe,
2464 'group_description': group.description_safe,
2465 'parent_group': group.parent_group.group_name if group.parent_group else None,
2465 'parent_group': group.parent_group.group_name if group.parent_group else None,
2466 'repositories': [x.repo_name for x in group.repositories],
2466 'repositories': [x.repo_name for x in group.repositories],
2467 'owner': group.user.username,
2467 'owner': group.user.username,
2468 }
2468 }
2469 return data
2469 return data
2470
2470
2471
2471
2472 class Permission(Base, BaseModel):
2472 class Permission(Base, BaseModel):
2473 __tablename__ = 'permissions'
2473 __tablename__ = 'permissions'
2474 __table_args__ = (
2474 __table_args__ = (
2475 Index('p_perm_name_idx', 'permission_name'),
2475 Index('p_perm_name_idx', 'permission_name'),
2476 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2476 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2477 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2477 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2478 )
2478 )
2479 PERMS = [
2479 PERMS = [
2480 ('hg.admin', _('RhodeCode Super Administrator')),
2480 ('hg.admin', _('RhodeCode Super Administrator')),
2481
2481
2482 ('repository.none', _('Repository no access')),
2482 ('repository.none', _('Repository no access')),
2483 ('repository.read', _('Repository read access')),
2483 ('repository.read', _('Repository read access')),
2484 ('repository.write', _('Repository write access')),
2484 ('repository.write', _('Repository write access')),
2485 ('repository.admin', _('Repository admin access')),
2485 ('repository.admin', _('Repository admin access')),
2486
2486
2487 ('group.none', _('Repository group no access')),
2487 ('group.none', _('Repository group no access')),
2488 ('group.read', _('Repository group read access')),
2488 ('group.read', _('Repository group read access')),
2489 ('group.write', _('Repository group write access')),
2489 ('group.write', _('Repository group write access')),
2490 ('group.admin', _('Repository group admin access')),
2490 ('group.admin', _('Repository group admin access')),
2491
2491
2492 ('usergroup.none', _('User group no access')),
2492 ('usergroup.none', _('User group no access')),
2493 ('usergroup.read', _('User group read access')),
2493 ('usergroup.read', _('User group read access')),
2494 ('usergroup.write', _('User group write access')),
2494 ('usergroup.write', _('User group write access')),
2495 ('usergroup.admin', _('User group admin access')),
2495 ('usergroup.admin', _('User group admin access')),
2496
2496
2497 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2497 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2498 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2498 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2499
2499
2500 ('hg.usergroup.create.false', _('User Group creation disabled')),
2500 ('hg.usergroup.create.false', _('User Group creation disabled')),
2501 ('hg.usergroup.create.true', _('User Group creation enabled')),
2501 ('hg.usergroup.create.true', _('User Group creation enabled')),
2502
2502
2503 ('hg.create.none', _('Repository creation disabled')),
2503 ('hg.create.none', _('Repository creation disabled')),
2504 ('hg.create.repository', _('Repository creation enabled')),
2504 ('hg.create.repository', _('Repository creation enabled')),
2505 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2505 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2506 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2506 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2507
2507
2508 ('hg.fork.none', _('Repository forking disabled')),
2508 ('hg.fork.none', _('Repository forking disabled')),
2509 ('hg.fork.repository', _('Repository forking enabled')),
2509 ('hg.fork.repository', _('Repository forking enabled')),
2510
2510
2511 ('hg.register.none', _('Registration disabled')),
2511 ('hg.register.none', _('Registration disabled')),
2512 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2512 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2513 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2513 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2514
2514
2515 ('hg.password_reset.enabled', _('Password reset enabled')),
2515 ('hg.password_reset.enabled', _('Password reset enabled')),
2516 ('hg.password_reset.hidden', _('Password reset hidden')),
2516 ('hg.password_reset.hidden', _('Password reset hidden')),
2517 ('hg.password_reset.disabled', _('Password reset disabled')),
2517 ('hg.password_reset.disabled', _('Password reset disabled')),
2518
2518
2519 ('hg.extern_activate.manual', _('Manual activation of external account')),
2519 ('hg.extern_activate.manual', _('Manual activation of external account')),
2520 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2520 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2521
2521
2522 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2522 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2523 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2523 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2524 ]
2524 ]
2525
2525
2526 # definition of system default permissions for DEFAULT user
2526 # definition of system default permissions for DEFAULT user
2527 DEFAULT_USER_PERMISSIONS = [
2527 DEFAULT_USER_PERMISSIONS = [
2528 'repository.read',
2528 'repository.read',
2529 'group.read',
2529 'group.read',
2530 'usergroup.read',
2530 'usergroup.read',
2531 'hg.create.repository',
2531 'hg.create.repository',
2532 'hg.repogroup.create.false',
2532 'hg.repogroup.create.false',
2533 'hg.usergroup.create.false',
2533 'hg.usergroup.create.false',
2534 'hg.create.write_on_repogroup.true',
2534 'hg.create.write_on_repogroup.true',
2535 'hg.fork.repository',
2535 'hg.fork.repository',
2536 'hg.register.manual_activate',
2536 'hg.register.manual_activate',
2537 'hg.password_reset.enabled',
2537 'hg.password_reset.enabled',
2538 'hg.extern_activate.auto',
2538 'hg.extern_activate.auto',
2539 'hg.inherit_default_perms.true',
2539 'hg.inherit_default_perms.true',
2540 ]
2540 ]
2541
2541
2542 # defines which permissions are more important higher the more important
2542 # defines which permissions are more important higher the more important
2543 # Weight defines which permissions are more important.
2543 # Weight defines which permissions are more important.
2544 # The higher number the more important.
2544 # The higher number the more important.
2545 PERM_WEIGHTS = {
2545 PERM_WEIGHTS = {
2546 'repository.none': 0,
2546 'repository.none': 0,
2547 'repository.read': 1,
2547 'repository.read': 1,
2548 'repository.write': 3,
2548 'repository.write': 3,
2549 'repository.admin': 4,
2549 'repository.admin': 4,
2550
2550
2551 'group.none': 0,
2551 'group.none': 0,
2552 'group.read': 1,
2552 'group.read': 1,
2553 'group.write': 3,
2553 'group.write': 3,
2554 'group.admin': 4,
2554 'group.admin': 4,
2555
2555
2556 'usergroup.none': 0,
2556 'usergroup.none': 0,
2557 'usergroup.read': 1,
2557 'usergroup.read': 1,
2558 'usergroup.write': 3,
2558 'usergroup.write': 3,
2559 'usergroup.admin': 4,
2559 'usergroup.admin': 4,
2560
2560
2561 'hg.repogroup.create.false': 0,
2561 'hg.repogroup.create.false': 0,
2562 'hg.repogroup.create.true': 1,
2562 'hg.repogroup.create.true': 1,
2563
2563
2564 'hg.usergroup.create.false': 0,
2564 'hg.usergroup.create.false': 0,
2565 'hg.usergroup.create.true': 1,
2565 'hg.usergroup.create.true': 1,
2566
2566
2567 'hg.fork.none': 0,
2567 'hg.fork.none': 0,
2568 'hg.fork.repository': 1,
2568 'hg.fork.repository': 1,
2569 'hg.create.none': 0,
2569 'hg.create.none': 0,
2570 'hg.create.repository': 1
2570 'hg.create.repository': 1
2571 }
2571 }
2572
2572
2573 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2573 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2574 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2574 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2575 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2575 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2576
2576
2577 def __unicode__(self):
2577 def __unicode__(self):
2578 return u"<%s('%s:%s')>" % (
2578 return u"<%s('%s:%s')>" % (
2579 self.__class__.__name__, self.permission_id, self.permission_name
2579 self.__class__.__name__, self.permission_id, self.permission_name
2580 )
2580 )
2581
2581
2582 @classmethod
2582 @classmethod
2583 def get_by_key(cls, key):
2583 def get_by_key(cls, key):
2584 return cls.query().filter(cls.permission_name == key).scalar()
2584 return cls.query().filter(cls.permission_name == key).scalar()
2585
2585
2586 @classmethod
2586 @classmethod
2587 def get_default_repo_perms(cls, user_id, repo_id=None):
2587 def get_default_repo_perms(cls, user_id, repo_id=None):
2588 q = Session().query(UserRepoToPerm, Repository, Permission)\
2588 q = Session().query(UserRepoToPerm, Repository, Permission)\
2589 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2589 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2590 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2590 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2591 .filter(UserRepoToPerm.user_id == user_id)
2591 .filter(UserRepoToPerm.user_id == user_id)
2592 if repo_id:
2592 if repo_id:
2593 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2593 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2594 return q.all()
2594 return q.all()
2595
2595
2596 @classmethod
2596 @classmethod
2597 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2597 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2598 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2598 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2599 .join(
2599 .join(
2600 Permission,
2600 Permission,
2601 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2601 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2602 .join(
2602 .join(
2603 Repository,
2603 Repository,
2604 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2604 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2605 .join(
2605 .join(
2606 UserGroup,
2606 UserGroup,
2607 UserGroupRepoToPerm.users_group_id ==
2607 UserGroupRepoToPerm.users_group_id ==
2608 UserGroup.users_group_id)\
2608 UserGroup.users_group_id)\
2609 .join(
2609 .join(
2610 UserGroupMember,
2610 UserGroupMember,
2611 UserGroupRepoToPerm.users_group_id ==
2611 UserGroupRepoToPerm.users_group_id ==
2612 UserGroupMember.users_group_id)\
2612 UserGroupMember.users_group_id)\
2613 .filter(
2613 .filter(
2614 UserGroupMember.user_id == user_id,
2614 UserGroupMember.user_id == user_id,
2615 UserGroup.users_group_active == true())
2615 UserGroup.users_group_active == true())
2616 if repo_id:
2616 if repo_id:
2617 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2617 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2618 return q.all()
2618 return q.all()
2619
2619
2620 @classmethod
2620 @classmethod
2621 def get_default_group_perms(cls, user_id, repo_group_id=None):
2621 def get_default_group_perms(cls, user_id, repo_group_id=None):
2622 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2622 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2623 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2623 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2624 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2624 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2625 .filter(UserRepoGroupToPerm.user_id == user_id)
2625 .filter(UserRepoGroupToPerm.user_id == user_id)
2626 if repo_group_id:
2626 if repo_group_id:
2627 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2627 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2628 return q.all()
2628 return q.all()
2629
2629
2630 @classmethod
2630 @classmethod
2631 def get_default_group_perms_from_user_group(
2631 def get_default_group_perms_from_user_group(
2632 cls, user_id, repo_group_id=None):
2632 cls, user_id, repo_group_id=None):
2633 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2633 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2634 .join(
2634 .join(
2635 Permission,
2635 Permission,
2636 UserGroupRepoGroupToPerm.permission_id ==
2636 UserGroupRepoGroupToPerm.permission_id ==
2637 Permission.permission_id)\
2637 Permission.permission_id)\
2638 .join(
2638 .join(
2639 RepoGroup,
2639 RepoGroup,
2640 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2640 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2641 .join(
2641 .join(
2642 UserGroup,
2642 UserGroup,
2643 UserGroupRepoGroupToPerm.users_group_id ==
2643 UserGroupRepoGroupToPerm.users_group_id ==
2644 UserGroup.users_group_id)\
2644 UserGroup.users_group_id)\
2645 .join(
2645 .join(
2646 UserGroupMember,
2646 UserGroupMember,
2647 UserGroupRepoGroupToPerm.users_group_id ==
2647 UserGroupRepoGroupToPerm.users_group_id ==
2648 UserGroupMember.users_group_id)\
2648 UserGroupMember.users_group_id)\
2649 .filter(
2649 .filter(
2650 UserGroupMember.user_id == user_id,
2650 UserGroupMember.user_id == user_id,
2651 UserGroup.users_group_active == true())
2651 UserGroup.users_group_active == true())
2652 if repo_group_id:
2652 if repo_group_id:
2653 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2653 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2654 return q.all()
2654 return q.all()
2655
2655
2656 @classmethod
2656 @classmethod
2657 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2657 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2658 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2658 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2659 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2659 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2660 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2660 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2661 .filter(UserUserGroupToPerm.user_id == user_id)
2661 .filter(UserUserGroupToPerm.user_id == user_id)
2662 if user_group_id:
2662 if user_group_id:
2663 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2663 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2664 return q.all()
2664 return q.all()
2665
2665
2666 @classmethod
2666 @classmethod
2667 def get_default_user_group_perms_from_user_group(
2667 def get_default_user_group_perms_from_user_group(
2668 cls, user_id, user_group_id=None):
2668 cls, user_id, user_group_id=None):
2669 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2669 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2670 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2670 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2671 .join(
2671 .join(
2672 Permission,
2672 Permission,
2673 UserGroupUserGroupToPerm.permission_id ==
2673 UserGroupUserGroupToPerm.permission_id ==
2674 Permission.permission_id)\
2674 Permission.permission_id)\
2675 .join(
2675 .join(
2676 TargetUserGroup,
2676 TargetUserGroup,
2677 UserGroupUserGroupToPerm.target_user_group_id ==
2677 UserGroupUserGroupToPerm.target_user_group_id ==
2678 TargetUserGroup.users_group_id)\
2678 TargetUserGroup.users_group_id)\
2679 .join(
2679 .join(
2680 UserGroup,
2680 UserGroup,
2681 UserGroupUserGroupToPerm.user_group_id ==
2681 UserGroupUserGroupToPerm.user_group_id ==
2682 UserGroup.users_group_id)\
2682 UserGroup.users_group_id)\
2683 .join(
2683 .join(
2684 UserGroupMember,
2684 UserGroupMember,
2685 UserGroupUserGroupToPerm.user_group_id ==
2685 UserGroupUserGroupToPerm.user_group_id ==
2686 UserGroupMember.users_group_id)\
2686 UserGroupMember.users_group_id)\
2687 .filter(
2687 .filter(
2688 UserGroupMember.user_id == user_id,
2688 UserGroupMember.user_id == user_id,
2689 UserGroup.users_group_active == true())
2689 UserGroup.users_group_active == true())
2690 if user_group_id:
2690 if user_group_id:
2691 q = q.filter(
2691 q = q.filter(
2692 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2692 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2693
2693
2694 return q.all()
2694 return q.all()
2695
2695
2696
2696
2697 class UserRepoToPerm(Base, BaseModel):
2697 class UserRepoToPerm(Base, BaseModel):
2698 __tablename__ = 'repo_to_perm'
2698 __tablename__ = 'repo_to_perm'
2699 __table_args__ = (
2699 __table_args__ = (
2700 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2700 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2701 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2701 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2702 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2702 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2703 )
2703 )
2704 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2704 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2705 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2705 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2706 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2706 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2707 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2707 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2708
2708
2709 user = relationship('User')
2709 user = relationship('User')
2710 repository = relationship('Repository')
2710 repository = relationship('Repository')
2711 permission = relationship('Permission')
2711 permission = relationship('Permission')
2712
2712
2713 @classmethod
2713 @classmethod
2714 def create(cls, user, repository, permission):
2714 def create(cls, user, repository, permission):
2715 n = cls()
2715 n = cls()
2716 n.user = user
2716 n.user = user
2717 n.repository = repository
2717 n.repository = repository
2718 n.permission = permission
2718 n.permission = permission
2719 Session().add(n)
2719 Session().add(n)
2720 return n
2720 return n
2721
2721
2722 def __unicode__(self):
2722 def __unicode__(self):
2723 return u'<%s => %s >' % (self.user, self.repository)
2723 return u'<%s => %s >' % (self.user, self.repository)
2724
2724
2725
2725
2726 class UserUserGroupToPerm(Base, BaseModel):
2726 class UserUserGroupToPerm(Base, BaseModel):
2727 __tablename__ = 'user_user_group_to_perm'
2727 __tablename__ = 'user_user_group_to_perm'
2728 __table_args__ = (
2728 __table_args__ = (
2729 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2729 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2730 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2730 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2731 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2731 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2732 )
2732 )
2733 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2734 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2735 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2735 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2736 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2736 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2737
2737
2738 user = relationship('User')
2738 user = relationship('User')
2739 user_group = relationship('UserGroup')
2739 user_group = relationship('UserGroup')
2740 permission = relationship('Permission')
2740 permission = relationship('Permission')
2741
2741
2742 @classmethod
2742 @classmethod
2743 def create(cls, user, user_group, permission):
2743 def create(cls, user, user_group, permission):
2744 n = cls()
2744 n = cls()
2745 n.user = user
2745 n.user = user
2746 n.user_group = user_group
2746 n.user_group = user_group
2747 n.permission = permission
2747 n.permission = permission
2748 Session().add(n)
2748 Session().add(n)
2749 return n
2749 return n
2750
2750
2751 def __unicode__(self):
2751 def __unicode__(self):
2752 return u'<%s => %s >' % (self.user, self.user_group)
2752 return u'<%s => %s >' % (self.user, self.user_group)
2753
2753
2754
2754
2755 class UserToPerm(Base, BaseModel):
2755 class UserToPerm(Base, BaseModel):
2756 __tablename__ = 'user_to_perm'
2756 __tablename__ = 'user_to_perm'
2757 __table_args__ = (
2757 __table_args__ = (
2758 UniqueConstraint('user_id', 'permission_id'),
2758 UniqueConstraint('user_id', 'permission_id'),
2759 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2759 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2760 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2760 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2761 )
2761 )
2762 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2762 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2763 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2763 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2764 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2764 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2765
2765
2766 user = relationship('User')
2766 user = relationship('User')
2767 permission = relationship('Permission', lazy='joined')
2767 permission = relationship('Permission', lazy='joined')
2768
2768
2769 def __unicode__(self):
2769 def __unicode__(self):
2770 return u'<%s => %s >' % (self.user, self.permission)
2770 return u'<%s => %s >' % (self.user, self.permission)
2771
2771
2772
2772
2773 class UserGroupRepoToPerm(Base, BaseModel):
2773 class UserGroupRepoToPerm(Base, BaseModel):
2774 __tablename__ = 'users_group_repo_to_perm'
2774 __tablename__ = 'users_group_repo_to_perm'
2775 __table_args__ = (
2775 __table_args__ = (
2776 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2776 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2777 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2777 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2778 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2778 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2779 )
2779 )
2780 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2780 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2781 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2781 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2782 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2782 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2783 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2783 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2784
2784
2785 users_group = relationship('UserGroup')
2785 users_group = relationship('UserGroup')
2786 permission = relationship('Permission')
2786 permission = relationship('Permission')
2787 repository = relationship('Repository')
2787 repository = relationship('Repository')
2788
2788
2789 @classmethod
2789 @classmethod
2790 def create(cls, users_group, repository, permission):
2790 def create(cls, users_group, repository, permission):
2791 n = cls()
2791 n = cls()
2792 n.users_group = users_group
2792 n.users_group = users_group
2793 n.repository = repository
2793 n.repository = repository
2794 n.permission = permission
2794 n.permission = permission
2795 Session().add(n)
2795 Session().add(n)
2796 return n
2796 return n
2797
2797
2798 def __unicode__(self):
2798 def __unicode__(self):
2799 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2799 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2800
2800
2801
2801
2802 class UserGroupUserGroupToPerm(Base, BaseModel):
2802 class UserGroupUserGroupToPerm(Base, BaseModel):
2803 __tablename__ = 'user_group_user_group_to_perm'
2803 __tablename__ = 'user_group_user_group_to_perm'
2804 __table_args__ = (
2804 __table_args__ = (
2805 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2805 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2806 CheckConstraint('target_user_group_id != user_group_id'),
2806 CheckConstraint('target_user_group_id != user_group_id'),
2807 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2807 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2808 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2808 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2809 )
2809 )
2810 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2810 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2811 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2811 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2812 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2812 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2813 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2813 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2814
2814
2815 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2815 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2816 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2816 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2817 permission = relationship('Permission')
2817 permission = relationship('Permission')
2818
2818
2819 @classmethod
2819 @classmethod
2820 def create(cls, target_user_group, user_group, permission):
2820 def create(cls, target_user_group, user_group, permission):
2821 n = cls()
2821 n = cls()
2822 n.target_user_group = target_user_group
2822 n.target_user_group = target_user_group
2823 n.user_group = user_group
2823 n.user_group = user_group
2824 n.permission = permission
2824 n.permission = permission
2825 Session().add(n)
2825 Session().add(n)
2826 return n
2826 return n
2827
2827
2828 def __unicode__(self):
2828 def __unicode__(self):
2829 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2829 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2830
2830
2831
2831
2832 class UserGroupToPerm(Base, BaseModel):
2832 class UserGroupToPerm(Base, BaseModel):
2833 __tablename__ = 'users_group_to_perm'
2833 __tablename__ = 'users_group_to_perm'
2834 __table_args__ = (
2834 __table_args__ = (
2835 UniqueConstraint('users_group_id', 'permission_id',),
2835 UniqueConstraint('users_group_id', 'permission_id',),
2836 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2836 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2837 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2837 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2838 )
2838 )
2839 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2839 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2840 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2840 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2841 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2841 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2842
2842
2843 users_group = relationship('UserGroup')
2843 users_group = relationship('UserGroup')
2844 permission = relationship('Permission')
2844 permission = relationship('Permission')
2845
2845
2846
2846
2847 class UserRepoGroupToPerm(Base, BaseModel):
2847 class UserRepoGroupToPerm(Base, BaseModel):
2848 __tablename__ = 'user_repo_group_to_perm'
2848 __tablename__ = 'user_repo_group_to_perm'
2849 __table_args__ = (
2849 __table_args__ = (
2850 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2850 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2851 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2851 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2852 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2852 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2853 )
2853 )
2854
2854
2855 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2855 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2856 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2856 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2857 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2857 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2858 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2858 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2859
2859
2860 user = relationship('User')
2860 user = relationship('User')
2861 group = relationship('RepoGroup')
2861 group = relationship('RepoGroup')
2862 permission = relationship('Permission')
2862 permission = relationship('Permission')
2863
2863
2864 @classmethod
2864 @classmethod
2865 def create(cls, user, repository_group, permission):
2865 def create(cls, user, repository_group, permission):
2866 n = cls()
2866 n = cls()
2867 n.user = user
2867 n.user = user
2868 n.group = repository_group
2868 n.group = repository_group
2869 n.permission = permission
2869 n.permission = permission
2870 Session().add(n)
2870 Session().add(n)
2871 return n
2871 return n
2872
2872
2873
2873
2874 class UserGroupRepoGroupToPerm(Base, BaseModel):
2874 class UserGroupRepoGroupToPerm(Base, BaseModel):
2875 __tablename__ = 'users_group_repo_group_to_perm'
2875 __tablename__ = 'users_group_repo_group_to_perm'
2876 __table_args__ = (
2876 __table_args__ = (
2877 UniqueConstraint('users_group_id', 'group_id'),
2877 UniqueConstraint('users_group_id', 'group_id'),
2878 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2878 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2879 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2879 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2880 )
2880 )
2881
2881
2882 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2882 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2883 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2883 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2884 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2884 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2885 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2885 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2886
2886
2887 users_group = relationship('UserGroup')
2887 users_group = relationship('UserGroup')
2888 permission = relationship('Permission')
2888 permission = relationship('Permission')
2889 group = relationship('RepoGroup')
2889 group = relationship('RepoGroup')
2890
2890
2891 @classmethod
2891 @classmethod
2892 def create(cls, user_group, repository_group, permission):
2892 def create(cls, user_group, repository_group, permission):
2893 n = cls()
2893 n = cls()
2894 n.users_group = user_group
2894 n.users_group = user_group
2895 n.group = repository_group
2895 n.group = repository_group
2896 n.permission = permission
2896 n.permission = permission
2897 Session().add(n)
2897 Session().add(n)
2898 return n
2898 return n
2899
2899
2900 def __unicode__(self):
2900 def __unicode__(self):
2901 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2901 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2902
2902
2903
2903
2904 class Statistics(Base, BaseModel):
2904 class Statistics(Base, BaseModel):
2905 __tablename__ = 'statistics'
2905 __tablename__ = 'statistics'
2906 __table_args__ = (
2906 __table_args__ = (
2907 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2907 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2908 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2908 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2909 )
2909 )
2910 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2910 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2911 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2911 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2912 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2912 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2913 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2913 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2914 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2914 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2915 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2915 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2916
2916
2917 repository = relationship('Repository', single_parent=True)
2917 repository = relationship('Repository', single_parent=True)
2918
2918
2919
2919
2920 class UserFollowing(Base, BaseModel):
2920 class UserFollowing(Base, BaseModel):
2921 __tablename__ = 'user_followings'
2921 __tablename__ = 'user_followings'
2922 __table_args__ = (
2922 __table_args__ = (
2923 UniqueConstraint('user_id', 'follows_repository_id'),
2923 UniqueConstraint('user_id', 'follows_repository_id'),
2924 UniqueConstraint('user_id', 'follows_user_id'),
2924 UniqueConstraint('user_id', 'follows_user_id'),
2925 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2925 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2926 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2926 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2927 )
2927 )
2928
2928
2929 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2929 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2930 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2930 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2931 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2931 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2932 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2932 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2933 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2933 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2934
2934
2935 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2935 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2936
2936
2937 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2937 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2938 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2938 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2939
2939
2940 @classmethod
2940 @classmethod
2941 def get_repo_followers(cls, repo_id):
2941 def get_repo_followers(cls, repo_id):
2942 return cls.query().filter(cls.follows_repo_id == repo_id)
2942 return cls.query().filter(cls.follows_repo_id == repo_id)
2943
2943
2944
2944
2945 class CacheKey(Base, BaseModel):
2945 class CacheKey(Base, BaseModel):
2946 __tablename__ = 'cache_invalidation'
2946 __tablename__ = 'cache_invalidation'
2947 __table_args__ = (
2947 __table_args__ = (
2948 UniqueConstraint('cache_key'),
2948 UniqueConstraint('cache_key'),
2949 Index('key_idx', 'cache_key'),
2949 Index('key_idx', 'cache_key'),
2950 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2950 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2951 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2951 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2952 )
2952 )
2953 CACHE_TYPE_ATOM = 'ATOM'
2953 CACHE_TYPE_ATOM = 'ATOM'
2954 CACHE_TYPE_RSS = 'RSS'
2954 CACHE_TYPE_RSS = 'RSS'
2955 CACHE_TYPE_README = 'README'
2955 CACHE_TYPE_README = 'README'
2956
2956
2957 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2957 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2958 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2958 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2959 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2959 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2960 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2960 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2961
2961
2962 def __init__(self, cache_key, cache_args=''):
2962 def __init__(self, cache_key, cache_args=''):
2963 self.cache_key = cache_key
2963 self.cache_key = cache_key
2964 self.cache_args = cache_args
2964 self.cache_args = cache_args
2965 self.cache_active = False
2965 self.cache_active = False
2966
2966
2967 def __unicode__(self):
2967 def __unicode__(self):
2968 return u"<%s('%s:%s[%s]')>" % (
2968 return u"<%s('%s:%s[%s]')>" % (
2969 self.__class__.__name__,
2969 self.__class__.__name__,
2970 self.cache_id, self.cache_key, self.cache_active)
2970 self.cache_id, self.cache_key, self.cache_active)
2971
2971
2972 def _cache_key_partition(self):
2972 def _cache_key_partition(self):
2973 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2973 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2974 return prefix, repo_name, suffix
2974 return prefix, repo_name, suffix
2975
2975
2976 def get_prefix(self):
2976 def get_prefix(self):
2977 """
2977 """
2978 Try to extract prefix from existing cache key. The key could consist
2978 Try to extract prefix from existing cache key. The key could consist
2979 of prefix, repo_name, suffix
2979 of prefix, repo_name, suffix
2980 """
2980 """
2981 # this returns prefix, repo_name, suffix
2981 # this returns prefix, repo_name, suffix
2982 return self._cache_key_partition()[0]
2982 return self._cache_key_partition()[0]
2983
2983
2984 def get_suffix(self):
2984 def get_suffix(self):
2985 """
2985 """
2986 get suffix that might have been used in _get_cache_key to
2986 get suffix that might have been used in _get_cache_key to
2987 generate self.cache_key. Only used for informational purposes
2987 generate self.cache_key. Only used for informational purposes
2988 in repo_edit.mako.
2988 in repo_edit.mako.
2989 """
2989 """
2990 # prefix, repo_name, suffix
2990 # prefix, repo_name, suffix
2991 return self._cache_key_partition()[2]
2991 return self._cache_key_partition()[2]
2992
2992
2993 @classmethod
2993 @classmethod
2994 def delete_all_cache(cls):
2994 def delete_all_cache(cls):
2995 """
2995 """
2996 Delete all cache keys from database.
2996 Delete all cache keys from database.
2997 Should only be run when all instances are down and all entries
2997 Should only be run when all instances are down and all entries
2998 thus stale.
2998 thus stale.
2999 """
2999 """
3000 cls.query().delete()
3000 cls.query().delete()
3001 Session().commit()
3001 Session().commit()
3002
3002
3003 @classmethod
3003 @classmethod
3004 def get_cache_key(cls, repo_name, cache_type):
3004 def get_cache_key(cls, repo_name, cache_type):
3005 """
3005 """
3006
3006
3007 Generate a cache key for this process of RhodeCode instance.
3007 Generate a cache key for this process of RhodeCode instance.
3008 Prefix most likely will be process id or maybe explicitly set
3008 Prefix most likely will be process id or maybe explicitly set
3009 instance_id from .ini file.
3009 instance_id from .ini file.
3010 """
3010 """
3011 import rhodecode
3011 import rhodecode
3012 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3012 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3013
3013
3014 repo_as_unicode = safe_unicode(repo_name)
3014 repo_as_unicode = safe_unicode(repo_name)
3015 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3015 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3016 if cache_type else repo_as_unicode
3016 if cache_type else repo_as_unicode
3017
3017
3018 return u'{}{}'.format(prefix, key)
3018 return u'{}{}'.format(prefix, key)
3019
3019
3020 @classmethod
3020 @classmethod
3021 def set_invalidate(cls, repo_name, delete=False):
3021 def set_invalidate(cls, repo_name, delete=False):
3022 """
3022 """
3023 Mark all caches of a repo as invalid in the database.
3023 Mark all caches of a repo as invalid in the database.
3024 """
3024 """
3025
3025
3026 try:
3026 try:
3027 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3027 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3028 if delete:
3028 if delete:
3029 log.debug('cache objects deleted for repo %s',
3029 log.debug('cache objects deleted for repo %s',
3030 safe_str(repo_name))
3030 safe_str(repo_name))
3031 qry.delete()
3031 qry.delete()
3032 else:
3032 else:
3033 log.debug('cache objects marked as invalid for repo %s',
3033 log.debug('cache objects marked as invalid for repo %s',
3034 safe_str(repo_name))
3034 safe_str(repo_name))
3035 qry.update({"cache_active": False})
3035 qry.update({"cache_active": False})
3036
3036
3037 Session().commit()
3037 Session().commit()
3038 except Exception:
3038 except Exception:
3039 log.exception(
3039 log.exception(
3040 'Cache key invalidation failed for repository %s',
3040 'Cache key invalidation failed for repository %s',
3041 safe_str(repo_name))
3041 safe_str(repo_name))
3042 Session().rollback()
3042 Session().rollback()
3043
3043
3044 @classmethod
3044 @classmethod
3045 def get_active_cache(cls, cache_key):
3045 def get_active_cache(cls, cache_key):
3046 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3046 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3047 if inv_obj:
3047 if inv_obj:
3048 return inv_obj
3048 return inv_obj
3049 return None
3049 return None
3050
3050
3051 @classmethod
3051 @classmethod
3052 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3052 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3053 thread_scoped=False):
3053 thread_scoped=False):
3054 """
3054 """
3055 @cache_region('long_term')
3055 @cache_region('long_term')
3056 def _heavy_calculation(cache_key):
3056 def _heavy_calculation(cache_key):
3057 return 'result'
3057 return 'result'
3058
3058
3059 cache_context = CacheKey.repo_context_cache(
3059 cache_context = CacheKey.repo_context_cache(
3060 _heavy_calculation, repo_name, cache_type)
3060 _heavy_calculation, repo_name, cache_type)
3061
3061
3062 with cache_context as context:
3062 with cache_context as context:
3063 context.invalidate()
3063 context.invalidate()
3064 computed = context.compute()
3064 computed = context.compute()
3065
3065
3066 assert computed == 'result'
3066 assert computed == 'result'
3067 """
3067 """
3068 from rhodecode.lib import caches
3068 from rhodecode.lib import caches
3069 return caches.InvalidationContext(
3069 return caches.InvalidationContext(
3070 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3070 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3071
3071
3072
3072
3073 class ChangesetComment(Base, BaseModel):
3073 class ChangesetComment(Base, BaseModel):
3074 __tablename__ = 'changeset_comments'
3074 __tablename__ = 'changeset_comments'
3075 __table_args__ = (
3075 __table_args__ = (
3076 Index('cc_revision_idx', 'revision'),
3076 Index('cc_revision_idx', 'revision'),
3077 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3077 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3078 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3078 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3079 )
3079 )
3080
3080
3081 COMMENT_OUTDATED = u'comment_outdated'
3081 COMMENT_OUTDATED = u'comment_outdated'
3082 COMMENT_TYPE_NOTE = u'note'
3082 COMMENT_TYPE_NOTE = u'note'
3083 COMMENT_TYPE_TODO = u'todo'
3083 COMMENT_TYPE_TODO = u'todo'
3084 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3084 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3085
3085
3086 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3086 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3087 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3087 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3088 revision = Column('revision', String(40), nullable=True)
3088 revision = Column('revision', String(40), nullable=True)
3089 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3089 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3090 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3090 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3091 line_no = Column('line_no', Unicode(10), nullable=True)
3091 line_no = Column('line_no', Unicode(10), nullable=True)
3092 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3092 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3093 f_path = Column('f_path', Unicode(1000), nullable=True)
3093 f_path = Column('f_path', Unicode(1000), nullable=True)
3094 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3094 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3095 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3095 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3096 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3096 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3097 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3097 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3098 renderer = Column('renderer', Unicode(64), nullable=True)
3098 renderer = Column('renderer', Unicode(64), nullable=True)
3099 display_state = Column('display_state', Unicode(128), nullable=True)
3099 display_state = Column('display_state', Unicode(128), nullable=True)
3100
3100
3101 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3101 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3102 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3102 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3103 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3103 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3104 author = relationship('User', lazy='joined')
3104 author = relationship('User', lazy='joined')
3105 repo = relationship('Repository')
3105 repo = relationship('Repository')
3106 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3106 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3107 pull_request = relationship('PullRequest', lazy='joined')
3107 pull_request = relationship('PullRequest', lazy='joined')
3108 pull_request_version = relationship('PullRequestVersion')
3108 pull_request_version = relationship('PullRequestVersion')
3109
3109
3110 @classmethod
3110 @classmethod
3111 def get_users(cls, revision=None, pull_request_id=None):
3111 def get_users(cls, revision=None, pull_request_id=None):
3112 """
3112 """
3113 Returns user associated with this ChangesetComment. ie those
3113 Returns user associated with this ChangesetComment. ie those
3114 who actually commented
3114 who actually commented
3115
3115
3116 :param cls:
3116 :param cls:
3117 :param revision:
3117 :param revision:
3118 """
3118 """
3119 q = Session().query(User)\
3119 q = Session().query(User)\
3120 .join(ChangesetComment.author)
3120 .join(ChangesetComment.author)
3121 if revision:
3121 if revision:
3122 q = q.filter(cls.revision == revision)
3122 q = q.filter(cls.revision == revision)
3123 elif pull_request_id:
3123 elif pull_request_id:
3124 q = q.filter(cls.pull_request_id == pull_request_id)
3124 q = q.filter(cls.pull_request_id == pull_request_id)
3125 return q.all()
3125 return q.all()
3126
3126
3127 @classmethod
3127 @classmethod
3128 def get_index_from_version(cls, pr_version, versions):
3128 def get_index_from_version(cls, pr_version, versions):
3129 num_versions = [x.pull_request_version_id for x in versions]
3129 num_versions = [x.pull_request_version_id for x in versions]
3130 try:
3130 try:
3131 return num_versions.index(pr_version) +1
3131 return num_versions.index(pr_version) +1
3132 except (IndexError, ValueError):
3132 except (IndexError, ValueError):
3133 return
3133 return
3134
3134
3135 @property
3135 @property
3136 def outdated(self):
3136 def outdated(self):
3137 return self.display_state == self.COMMENT_OUTDATED
3137 return self.display_state == self.COMMENT_OUTDATED
3138
3138
3139 def outdated_at_version(self, version):
3139 def outdated_at_version(self, version):
3140 """
3140 """
3141 Checks if comment is outdated for given pull request version
3141 Checks if comment is outdated for given pull request version
3142 """
3142 """
3143 return self.outdated and self.pull_request_version_id != version
3143 return self.outdated and self.pull_request_version_id != version
3144
3144
3145 def older_than_version(self, version):
3145 def older_than_version(self, version):
3146 """
3146 """
3147 Checks if comment is made from previous version than given
3147 Checks if comment is made from previous version than given
3148 """
3148 """
3149 if version is None:
3149 if version is None:
3150 return self.pull_request_version_id is not None
3150 return self.pull_request_version_id is not None
3151
3151
3152 return self.pull_request_version_id < version
3152 return self.pull_request_version_id < version
3153
3153
3154 @property
3154 @property
3155 def resolved(self):
3155 def resolved(self):
3156 return self.resolved_by[0] if self.resolved_by else None
3156 return self.resolved_by[0] if self.resolved_by else None
3157
3157
3158 @property
3158 @property
3159 def is_todo(self):
3159 def is_todo(self):
3160 return self.comment_type == self.COMMENT_TYPE_TODO
3160 return self.comment_type == self.COMMENT_TYPE_TODO
3161
3161
3162 @property
3162 @property
3163 def is_inline(self):
3163 def is_inline(self):
3164 return self.line_no and self.f_path
3164 return self.line_no and self.f_path
3165
3165
3166 def get_index_version(self, versions):
3166 def get_index_version(self, versions):
3167 return self.get_index_from_version(
3167 return self.get_index_from_version(
3168 self.pull_request_version_id, versions)
3168 self.pull_request_version_id, versions)
3169
3169
3170 def __repr__(self):
3170 def __repr__(self):
3171 if self.comment_id:
3171 if self.comment_id:
3172 return '<DB:Comment #%s>' % self.comment_id
3172 return '<DB:Comment #%s>' % self.comment_id
3173 else:
3173 else:
3174 return '<DB:Comment at %#x>' % id(self)
3174 return '<DB:Comment at %#x>' % id(self)
3175
3175
3176 def get_api_data(self):
3176 def get_api_data(self):
3177 comment = self
3177 comment = self
3178 data = {
3178 data = {
3179 'comment_id': comment.comment_id,
3179 'comment_id': comment.comment_id,
3180 'comment_type': comment.comment_type,
3180 'comment_type': comment.comment_type,
3181 'comment_text': comment.text,
3181 'comment_text': comment.text,
3182 'comment_status': comment.status_change,
3182 'comment_status': comment.status_change,
3183 'comment_f_path': comment.f_path,
3183 'comment_f_path': comment.f_path,
3184 'comment_lineno': comment.line_no,
3184 'comment_lineno': comment.line_no,
3185 'comment_author': comment.author,
3185 'comment_author': comment.author,
3186 'comment_created_on': comment.created_on
3186 'comment_created_on': comment.created_on
3187 }
3187 }
3188 return data
3188 return data
3189
3189
3190 def __json__(self):
3190 def __json__(self):
3191 data = dict()
3191 data = dict()
3192 data.update(self.get_api_data())
3192 data.update(self.get_api_data())
3193 return data
3193 return data
3194
3194
3195
3195
3196 class ChangesetStatus(Base, BaseModel):
3196 class ChangesetStatus(Base, BaseModel):
3197 __tablename__ = 'changeset_statuses'
3197 __tablename__ = 'changeset_statuses'
3198 __table_args__ = (
3198 __table_args__ = (
3199 Index('cs_revision_idx', 'revision'),
3199 Index('cs_revision_idx', 'revision'),
3200 Index('cs_version_idx', 'version'),
3200 Index('cs_version_idx', 'version'),
3201 UniqueConstraint('repo_id', 'revision', 'version'),
3201 UniqueConstraint('repo_id', 'revision', 'version'),
3202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3204 )
3204 )
3205 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3205 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3206 STATUS_APPROVED = 'approved'
3206 STATUS_APPROVED = 'approved'
3207 STATUS_REJECTED = 'rejected'
3207 STATUS_REJECTED = 'rejected'
3208 STATUS_UNDER_REVIEW = 'under_review'
3208 STATUS_UNDER_REVIEW = 'under_review'
3209
3209
3210 STATUSES = [
3210 STATUSES = [
3211 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3211 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3212 (STATUS_APPROVED, _("Approved")),
3212 (STATUS_APPROVED, _("Approved")),
3213 (STATUS_REJECTED, _("Rejected")),
3213 (STATUS_REJECTED, _("Rejected")),
3214 (STATUS_UNDER_REVIEW, _("Under Review")),
3214 (STATUS_UNDER_REVIEW, _("Under Review")),
3215 ]
3215 ]
3216
3216
3217 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3217 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3218 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3218 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3219 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3219 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3220 revision = Column('revision', String(40), nullable=False)
3220 revision = Column('revision', String(40), nullable=False)
3221 status = Column('status', String(128), nullable=False, default=DEFAULT)
3221 status = Column('status', String(128), nullable=False, default=DEFAULT)
3222 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3222 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3223 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3223 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3224 version = Column('version', Integer(), nullable=False, default=0)
3224 version = Column('version', Integer(), nullable=False, default=0)
3225 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3225 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3226
3226
3227 author = relationship('User', lazy='joined')
3227 author = relationship('User', lazy='joined')
3228 repo = relationship('Repository')
3228 repo = relationship('Repository')
3229 comment = relationship('ChangesetComment', lazy='joined')
3229 comment = relationship('ChangesetComment', lazy='joined')
3230 pull_request = relationship('PullRequest', lazy='joined')
3230 pull_request = relationship('PullRequest', lazy='joined')
3231
3231
3232 def __unicode__(self):
3232 def __unicode__(self):
3233 return u"<%s('%s[v%s]:%s')>" % (
3233 return u"<%s('%s[v%s]:%s')>" % (
3234 self.__class__.__name__,
3234 self.__class__.__name__,
3235 self.status, self.version, self.author
3235 self.status, self.version, self.author
3236 )
3236 )
3237
3237
3238 @classmethod
3238 @classmethod
3239 def get_status_lbl(cls, value):
3239 def get_status_lbl(cls, value):
3240 return dict(cls.STATUSES).get(value)
3240 return dict(cls.STATUSES).get(value)
3241
3241
3242 @property
3242 @property
3243 def status_lbl(self):
3243 def status_lbl(self):
3244 return ChangesetStatus.get_status_lbl(self.status)
3244 return ChangesetStatus.get_status_lbl(self.status)
3245
3245
3246 def get_api_data(self):
3246 def get_api_data(self):
3247 status = self
3247 status = self
3248 data = {
3248 data = {
3249 'status_id': status.changeset_status_id,
3249 'status_id': status.changeset_status_id,
3250 'status': status.status,
3250 'status': status.status,
3251 }
3251 }
3252 return data
3252 return data
3253
3253
3254 def __json__(self):
3254 def __json__(self):
3255 data = dict()
3255 data = dict()
3256 data.update(self.get_api_data())
3256 data.update(self.get_api_data())
3257 return data
3257 return data
3258
3258
3259
3259
3260 class _PullRequestBase(BaseModel):
3260 class _PullRequestBase(BaseModel):
3261 """
3261 """
3262 Common attributes of pull request and version entries.
3262 Common attributes of pull request and version entries.
3263 """
3263 """
3264
3264
3265 # .status values
3265 # .status values
3266 STATUS_NEW = u'new'
3266 STATUS_NEW = u'new'
3267 STATUS_OPEN = u'open'
3267 STATUS_OPEN = u'open'
3268 STATUS_CLOSED = u'closed'
3268 STATUS_CLOSED = u'closed'
3269
3269
3270 title = Column('title', Unicode(255), nullable=True)
3270 title = Column('title', Unicode(255), nullable=True)
3271 description = Column(
3271 description = Column(
3272 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3272 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3273 nullable=True)
3273 nullable=True)
3274 # new/open/closed status of pull request (not approve/reject/etc)
3274 # new/open/closed status of pull request (not approve/reject/etc)
3275 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3275 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3276 created_on = Column(
3276 created_on = Column(
3277 'created_on', DateTime(timezone=False), nullable=False,
3277 'created_on', DateTime(timezone=False), nullable=False,
3278 default=datetime.datetime.now)
3278 default=datetime.datetime.now)
3279 updated_on = Column(
3279 updated_on = Column(
3280 'updated_on', DateTime(timezone=False), nullable=False,
3280 'updated_on', DateTime(timezone=False), nullable=False,
3281 default=datetime.datetime.now)
3281 default=datetime.datetime.now)
3282
3282
3283 @declared_attr
3283 @declared_attr
3284 def user_id(cls):
3284 def user_id(cls):
3285 return Column(
3285 return Column(
3286 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3286 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3287 unique=None)
3287 unique=None)
3288
3288
3289 # 500 revisions max
3289 # 500 revisions max
3290 _revisions = Column(
3290 _revisions = Column(
3291 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3291 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3292
3292
3293 @declared_attr
3293 @declared_attr
3294 def source_repo_id(cls):
3294 def source_repo_id(cls):
3295 # TODO: dan: rename column to source_repo_id
3295 # TODO: dan: rename column to source_repo_id
3296 return Column(
3296 return Column(
3297 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3297 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3298 nullable=False)
3298 nullable=False)
3299
3299
3300 source_ref = Column('org_ref', Unicode(255), nullable=False)
3300 source_ref = Column('org_ref', Unicode(255), nullable=False)
3301
3301
3302 @declared_attr
3302 @declared_attr
3303 def target_repo_id(cls):
3303 def target_repo_id(cls):
3304 # TODO: dan: rename column to target_repo_id
3304 # TODO: dan: rename column to target_repo_id
3305 return Column(
3305 return Column(
3306 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3306 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3307 nullable=False)
3307 nullable=False)
3308
3308
3309 target_ref = Column('other_ref', Unicode(255), nullable=False)
3309 target_ref = Column('other_ref', Unicode(255), nullable=False)
3310 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3310 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3311
3311
3312 # TODO: dan: rename column to last_merge_source_rev
3312 # TODO: dan: rename column to last_merge_source_rev
3313 _last_merge_source_rev = Column(
3313 _last_merge_source_rev = Column(
3314 'last_merge_org_rev', String(40), nullable=True)
3314 'last_merge_org_rev', String(40), nullable=True)
3315 # TODO: dan: rename column to last_merge_target_rev
3315 # TODO: dan: rename column to last_merge_target_rev
3316 _last_merge_target_rev = Column(
3316 _last_merge_target_rev = Column(
3317 'last_merge_other_rev', String(40), nullable=True)
3317 'last_merge_other_rev', String(40), nullable=True)
3318 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3318 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3319 merge_rev = Column('merge_rev', String(40), nullable=True)
3319 merge_rev = Column('merge_rev', String(40), nullable=True)
3320
3320
3321 reviewer_data = Column(
3321 reviewer_data = Column(
3322 'reviewer_data_json', MutationObj.as_mutable(
3322 'reviewer_data_json', MutationObj.as_mutable(
3323 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3323 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3324
3324
3325 @property
3325 @property
3326 def reviewer_data_json(self):
3326 def reviewer_data_json(self):
3327 return json.dumps(self.reviewer_data)
3327 return json.dumps(self.reviewer_data)
3328
3328
3329 @hybrid_property
3329 @hybrid_property
3330 def description_safe(self):
3330 def description_safe(self):
3331 from rhodecode.lib import helpers as h
3331 from rhodecode.lib import helpers as h
3332 return h.escape(self.description)
3332 return h.escape(self.description)
3333
3333
3334 @hybrid_property
3334 @hybrid_property
3335 def revisions(self):
3335 def revisions(self):
3336 return self._revisions.split(':') if self._revisions else []
3336 return self._revisions.split(':') if self._revisions else []
3337
3337
3338 @revisions.setter
3338 @revisions.setter
3339 def revisions(self, val):
3339 def revisions(self, val):
3340 self._revisions = ':'.join(val)
3340 self._revisions = ':'.join(val)
3341
3341
3342 @hybrid_property
3343 def last_merge_status(self):
3344 return safe_int(self._last_merge_status)
3345
3346 @last_merge_status.setter
3347 def last_merge_status(self, val):
3348 self._last_merge_status = val
3349
3342 @declared_attr
3350 @declared_attr
3343 def author(cls):
3351 def author(cls):
3344 return relationship('User', lazy='joined')
3352 return relationship('User', lazy='joined')
3345
3353
3346 @declared_attr
3354 @declared_attr
3347 def source_repo(cls):
3355 def source_repo(cls):
3348 return relationship(
3356 return relationship(
3349 'Repository',
3357 'Repository',
3350 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3358 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3351
3359
3352 @property
3360 @property
3353 def source_ref_parts(self):
3361 def source_ref_parts(self):
3354 return self.unicode_to_reference(self.source_ref)
3362 return self.unicode_to_reference(self.source_ref)
3355
3363
3356 @declared_attr
3364 @declared_attr
3357 def target_repo(cls):
3365 def target_repo(cls):
3358 return relationship(
3366 return relationship(
3359 'Repository',
3367 'Repository',
3360 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3368 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3361
3369
3362 @property
3370 @property
3363 def target_ref_parts(self):
3371 def target_ref_parts(self):
3364 return self.unicode_to_reference(self.target_ref)
3372 return self.unicode_to_reference(self.target_ref)
3365
3373
3366 @property
3374 @property
3367 def shadow_merge_ref(self):
3375 def shadow_merge_ref(self):
3368 return self.unicode_to_reference(self._shadow_merge_ref)
3376 return self.unicode_to_reference(self._shadow_merge_ref)
3369
3377
3370 @shadow_merge_ref.setter
3378 @shadow_merge_ref.setter
3371 def shadow_merge_ref(self, ref):
3379 def shadow_merge_ref(self, ref):
3372 self._shadow_merge_ref = self.reference_to_unicode(ref)
3380 self._shadow_merge_ref = self.reference_to_unicode(ref)
3373
3381
3374 def unicode_to_reference(self, raw):
3382 def unicode_to_reference(self, raw):
3375 """
3383 """
3376 Convert a unicode (or string) to a reference object.
3384 Convert a unicode (or string) to a reference object.
3377 If unicode evaluates to False it returns None.
3385 If unicode evaluates to False it returns None.
3378 """
3386 """
3379 if raw:
3387 if raw:
3380 refs = raw.split(':')
3388 refs = raw.split(':')
3381 return Reference(*refs)
3389 return Reference(*refs)
3382 else:
3390 else:
3383 return None
3391 return None
3384
3392
3385 def reference_to_unicode(self, ref):
3393 def reference_to_unicode(self, ref):
3386 """
3394 """
3387 Convert a reference object to unicode.
3395 Convert a reference object to unicode.
3388 If reference is None it returns None.
3396 If reference is None it returns None.
3389 """
3397 """
3390 if ref:
3398 if ref:
3391 return u':'.join(ref)
3399 return u':'.join(ref)
3392 else:
3400 else:
3393 return None
3401 return None
3394
3402
3395 def get_api_data(self, with_merge_state=True):
3403 def get_api_data(self, with_merge_state=True):
3396 from rhodecode.model.pull_request import PullRequestModel
3404 from rhodecode.model.pull_request import PullRequestModel
3397
3405
3398 pull_request = self
3406 pull_request = self
3399 if with_merge_state:
3407 if with_merge_state:
3400 merge_status = PullRequestModel().merge_status(pull_request)
3408 merge_status = PullRequestModel().merge_status(pull_request)
3401 merge_state = {
3409 merge_state = {
3402 'status': merge_status[0],
3410 'status': merge_status[0],
3403 'message': safe_unicode(merge_status[1]),
3411 'message': safe_unicode(merge_status[1]),
3404 }
3412 }
3405 else:
3413 else:
3406 merge_state = {'status': 'not_available',
3414 merge_state = {'status': 'not_available',
3407 'message': 'not_available'}
3415 'message': 'not_available'}
3408
3416
3409 merge_data = {
3417 merge_data = {
3410 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3418 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3411 'reference': (
3419 'reference': (
3412 pull_request.shadow_merge_ref._asdict()
3420 pull_request.shadow_merge_ref._asdict()
3413 if pull_request.shadow_merge_ref else None),
3421 if pull_request.shadow_merge_ref else None),
3414 }
3422 }
3415
3423
3416 data = {
3424 data = {
3417 'pull_request_id': pull_request.pull_request_id,
3425 'pull_request_id': pull_request.pull_request_id,
3418 'url': PullRequestModel().get_url(pull_request),
3426 'url': PullRequestModel().get_url(pull_request),
3419 'title': pull_request.title,
3427 'title': pull_request.title,
3420 'description': pull_request.description,
3428 'description': pull_request.description,
3421 'status': pull_request.status,
3429 'status': pull_request.status,
3422 'created_on': pull_request.created_on,
3430 'created_on': pull_request.created_on,
3423 'updated_on': pull_request.updated_on,
3431 'updated_on': pull_request.updated_on,
3424 'commit_ids': pull_request.revisions,
3432 'commit_ids': pull_request.revisions,
3425 'review_status': pull_request.calculated_review_status(),
3433 'review_status': pull_request.calculated_review_status(),
3426 'mergeable': merge_state,
3434 'mergeable': merge_state,
3427 'source': {
3435 'source': {
3428 'clone_url': pull_request.source_repo.clone_url(),
3436 'clone_url': pull_request.source_repo.clone_url(),
3429 'repository': pull_request.source_repo.repo_name,
3437 'repository': pull_request.source_repo.repo_name,
3430 'reference': {
3438 'reference': {
3431 'name': pull_request.source_ref_parts.name,
3439 'name': pull_request.source_ref_parts.name,
3432 'type': pull_request.source_ref_parts.type,
3440 'type': pull_request.source_ref_parts.type,
3433 'commit_id': pull_request.source_ref_parts.commit_id,
3441 'commit_id': pull_request.source_ref_parts.commit_id,
3434 },
3442 },
3435 },
3443 },
3436 'target': {
3444 'target': {
3437 'clone_url': pull_request.target_repo.clone_url(),
3445 'clone_url': pull_request.target_repo.clone_url(),
3438 'repository': pull_request.target_repo.repo_name,
3446 'repository': pull_request.target_repo.repo_name,
3439 'reference': {
3447 'reference': {
3440 'name': pull_request.target_ref_parts.name,
3448 'name': pull_request.target_ref_parts.name,
3441 'type': pull_request.target_ref_parts.type,
3449 'type': pull_request.target_ref_parts.type,
3442 'commit_id': pull_request.target_ref_parts.commit_id,
3450 'commit_id': pull_request.target_ref_parts.commit_id,
3443 },
3451 },
3444 },
3452 },
3445 'merge': merge_data,
3453 'merge': merge_data,
3446 'author': pull_request.author.get_api_data(include_secrets=False,
3454 'author': pull_request.author.get_api_data(include_secrets=False,
3447 details='basic'),
3455 details='basic'),
3448 'reviewers': [
3456 'reviewers': [
3449 {
3457 {
3450 'user': reviewer.get_api_data(include_secrets=False,
3458 'user': reviewer.get_api_data(include_secrets=False,
3451 details='basic'),
3459 details='basic'),
3452 'reasons': reasons,
3460 'reasons': reasons,
3453 'review_status': st[0][1].status if st else 'not_reviewed',
3461 'review_status': st[0][1].status if st else 'not_reviewed',
3454 }
3462 }
3455 for reviewer, reasons, mandatory, st in
3463 for reviewer, reasons, mandatory, st in
3456 pull_request.reviewers_statuses()
3464 pull_request.reviewers_statuses()
3457 ]
3465 ]
3458 }
3466 }
3459
3467
3460 return data
3468 return data
3461
3469
3462
3470
3463 class PullRequest(Base, _PullRequestBase):
3471 class PullRequest(Base, _PullRequestBase):
3464 __tablename__ = 'pull_requests'
3472 __tablename__ = 'pull_requests'
3465 __table_args__ = (
3473 __table_args__ = (
3466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3474 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3475 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3468 )
3476 )
3469
3477
3470 pull_request_id = Column(
3478 pull_request_id = Column(
3471 'pull_request_id', Integer(), nullable=False, primary_key=True)
3479 'pull_request_id', Integer(), nullable=False, primary_key=True)
3472
3480
3473 def __repr__(self):
3481 def __repr__(self):
3474 if self.pull_request_id:
3482 if self.pull_request_id:
3475 return '<DB:PullRequest #%s>' % self.pull_request_id
3483 return '<DB:PullRequest #%s>' % self.pull_request_id
3476 else:
3484 else:
3477 return '<DB:PullRequest at %#x>' % id(self)
3485 return '<DB:PullRequest at %#x>' % id(self)
3478
3486
3479 reviewers = relationship('PullRequestReviewers',
3487 reviewers = relationship('PullRequestReviewers',
3480 cascade="all, delete, delete-orphan")
3488 cascade="all, delete, delete-orphan")
3481 statuses = relationship('ChangesetStatus',
3489 statuses = relationship('ChangesetStatus',
3482 cascade="all, delete, delete-orphan")
3490 cascade="all, delete, delete-orphan")
3483 comments = relationship('ChangesetComment',
3491 comments = relationship('ChangesetComment',
3484 cascade="all, delete, delete-orphan")
3492 cascade="all, delete, delete-orphan")
3485 versions = relationship('PullRequestVersion',
3493 versions = relationship('PullRequestVersion',
3486 cascade="all, delete, delete-orphan",
3494 cascade="all, delete, delete-orphan",
3487 lazy='dynamic')
3495 lazy='dynamic')
3488
3496
3489 @classmethod
3497 @classmethod
3490 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3498 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3491 internal_methods=None):
3499 internal_methods=None):
3492
3500
3493 class PullRequestDisplay(object):
3501 class PullRequestDisplay(object):
3494 """
3502 """
3495 Special object wrapper for showing PullRequest data via Versions
3503 Special object wrapper for showing PullRequest data via Versions
3496 It mimics PR object as close as possible. This is read only object
3504 It mimics PR object as close as possible. This is read only object
3497 just for display
3505 just for display
3498 """
3506 """
3499
3507
3500 def __init__(self, attrs, internal=None):
3508 def __init__(self, attrs, internal=None):
3501 self.attrs = attrs
3509 self.attrs = attrs
3502 # internal have priority over the given ones via attrs
3510 # internal have priority over the given ones via attrs
3503 self.internal = internal or ['versions']
3511 self.internal = internal or ['versions']
3504
3512
3505 def __getattr__(self, item):
3513 def __getattr__(self, item):
3506 if item in self.internal:
3514 if item in self.internal:
3507 return getattr(self, item)
3515 return getattr(self, item)
3508 try:
3516 try:
3509 return self.attrs[item]
3517 return self.attrs[item]
3510 except KeyError:
3518 except KeyError:
3511 raise AttributeError(
3519 raise AttributeError(
3512 '%s object has no attribute %s' % (self, item))
3520 '%s object has no attribute %s' % (self, item))
3513
3521
3514 def __repr__(self):
3522 def __repr__(self):
3515 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3523 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3516
3524
3517 def versions(self):
3525 def versions(self):
3518 return pull_request_obj.versions.order_by(
3526 return pull_request_obj.versions.order_by(
3519 PullRequestVersion.pull_request_version_id).all()
3527 PullRequestVersion.pull_request_version_id).all()
3520
3528
3521 def is_closed(self):
3529 def is_closed(self):
3522 return pull_request_obj.is_closed()
3530 return pull_request_obj.is_closed()
3523
3531
3524 @property
3532 @property
3525 def pull_request_version_id(self):
3533 def pull_request_version_id(self):
3526 return getattr(pull_request_obj, 'pull_request_version_id', None)
3534 return getattr(pull_request_obj, 'pull_request_version_id', None)
3527
3535
3528 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3536 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3529
3537
3530 attrs.author = StrictAttributeDict(
3538 attrs.author = StrictAttributeDict(
3531 pull_request_obj.author.get_api_data())
3539 pull_request_obj.author.get_api_data())
3532 if pull_request_obj.target_repo:
3540 if pull_request_obj.target_repo:
3533 attrs.target_repo = StrictAttributeDict(
3541 attrs.target_repo = StrictAttributeDict(
3534 pull_request_obj.target_repo.get_api_data())
3542 pull_request_obj.target_repo.get_api_data())
3535 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3543 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3536
3544
3537 if pull_request_obj.source_repo:
3545 if pull_request_obj.source_repo:
3538 attrs.source_repo = StrictAttributeDict(
3546 attrs.source_repo = StrictAttributeDict(
3539 pull_request_obj.source_repo.get_api_data())
3547 pull_request_obj.source_repo.get_api_data())
3540 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3548 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3541
3549
3542 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3550 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3543 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3551 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3544 attrs.revisions = pull_request_obj.revisions
3552 attrs.revisions = pull_request_obj.revisions
3545
3553
3546 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3554 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3547 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3555 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3548 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3556 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3549
3557
3550 return PullRequestDisplay(attrs, internal=internal_methods)
3558 return PullRequestDisplay(attrs, internal=internal_methods)
3551
3559
3552 def is_closed(self):
3560 def is_closed(self):
3553 return self.status == self.STATUS_CLOSED
3561 return self.status == self.STATUS_CLOSED
3554
3562
3555 def __json__(self):
3563 def __json__(self):
3556 return {
3564 return {
3557 'revisions': self.revisions,
3565 'revisions': self.revisions,
3558 }
3566 }
3559
3567
3560 def calculated_review_status(self):
3568 def calculated_review_status(self):
3561 from rhodecode.model.changeset_status import ChangesetStatusModel
3569 from rhodecode.model.changeset_status import ChangesetStatusModel
3562 return ChangesetStatusModel().calculated_review_status(self)
3570 return ChangesetStatusModel().calculated_review_status(self)
3563
3571
3564 def reviewers_statuses(self):
3572 def reviewers_statuses(self):
3565 from rhodecode.model.changeset_status import ChangesetStatusModel
3573 from rhodecode.model.changeset_status import ChangesetStatusModel
3566 return ChangesetStatusModel().reviewers_statuses(self)
3574 return ChangesetStatusModel().reviewers_statuses(self)
3567
3575
3568 @property
3576 @property
3569 def workspace_id(self):
3577 def workspace_id(self):
3570 from rhodecode.model.pull_request import PullRequestModel
3578 from rhodecode.model.pull_request import PullRequestModel
3571 return PullRequestModel()._workspace_id(self)
3579 return PullRequestModel()._workspace_id(self)
3572
3580
3573 def get_shadow_repo(self):
3581 def get_shadow_repo(self):
3574 workspace_id = self.workspace_id
3582 workspace_id = self.workspace_id
3575 vcs_obj = self.target_repo.scm_instance()
3583 vcs_obj = self.target_repo.scm_instance()
3576 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3584 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3577 workspace_id)
3585 workspace_id)
3578 return vcs_obj._get_shadow_instance(shadow_repository_path)
3586 return vcs_obj._get_shadow_instance(shadow_repository_path)
3579
3587
3580
3588
3581 class PullRequestVersion(Base, _PullRequestBase):
3589 class PullRequestVersion(Base, _PullRequestBase):
3582 __tablename__ = 'pull_request_versions'
3590 __tablename__ = 'pull_request_versions'
3583 __table_args__ = (
3591 __table_args__ = (
3584 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3592 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3585 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3593 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3586 )
3594 )
3587
3595
3588 pull_request_version_id = Column(
3596 pull_request_version_id = Column(
3589 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3597 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3590 pull_request_id = Column(
3598 pull_request_id = Column(
3591 'pull_request_id', Integer(),
3599 'pull_request_id', Integer(),
3592 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3600 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3593 pull_request = relationship('PullRequest')
3601 pull_request = relationship('PullRequest')
3594
3602
3595 def __repr__(self):
3603 def __repr__(self):
3596 if self.pull_request_version_id:
3604 if self.pull_request_version_id:
3597 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3605 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3598 else:
3606 else:
3599 return '<DB:PullRequestVersion at %#x>' % id(self)
3607 return '<DB:PullRequestVersion at %#x>' % id(self)
3600
3608
3601 @property
3609 @property
3602 def reviewers(self):
3610 def reviewers(self):
3603 return self.pull_request.reviewers
3611 return self.pull_request.reviewers
3604
3612
3605 @property
3613 @property
3606 def versions(self):
3614 def versions(self):
3607 return self.pull_request.versions
3615 return self.pull_request.versions
3608
3616
3609 def is_closed(self):
3617 def is_closed(self):
3610 # calculate from original
3618 # calculate from original
3611 return self.pull_request.status == self.STATUS_CLOSED
3619 return self.pull_request.status == self.STATUS_CLOSED
3612
3620
3613 def calculated_review_status(self):
3621 def calculated_review_status(self):
3614 return self.pull_request.calculated_review_status()
3622 return self.pull_request.calculated_review_status()
3615
3623
3616 def reviewers_statuses(self):
3624 def reviewers_statuses(self):
3617 return self.pull_request.reviewers_statuses()
3625 return self.pull_request.reviewers_statuses()
3618
3626
3619
3627
3620 class PullRequestReviewers(Base, BaseModel):
3628 class PullRequestReviewers(Base, BaseModel):
3621 __tablename__ = 'pull_request_reviewers'
3629 __tablename__ = 'pull_request_reviewers'
3622 __table_args__ = (
3630 __table_args__ = (
3623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3631 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3632 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3625 )
3633 )
3626
3634
3627 @hybrid_property
3635 @hybrid_property
3628 def reasons(self):
3636 def reasons(self):
3629 if not self._reasons:
3637 if not self._reasons:
3630 return []
3638 return []
3631 return self._reasons
3639 return self._reasons
3632
3640
3633 @reasons.setter
3641 @reasons.setter
3634 def reasons(self, val):
3642 def reasons(self, val):
3635 val = val or []
3643 val = val or []
3636 if any(not isinstance(x, basestring) for x in val):
3644 if any(not isinstance(x, basestring) for x in val):
3637 raise Exception('invalid reasons type, must be list of strings')
3645 raise Exception('invalid reasons type, must be list of strings')
3638 self._reasons = val
3646 self._reasons = val
3639
3647
3640 pull_requests_reviewers_id = Column(
3648 pull_requests_reviewers_id = Column(
3641 'pull_requests_reviewers_id', Integer(), nullable=False,
3649 'pull_requests_reviewers_id', Integer(), nullable=False,
3642 primary_key=True)
3650 primary_key=True)
3643 pull_request_id = Column(
3651 pull_request_id = Column(
3644 "pull_request_id", Integer(),
3652 "pull_request_id", Integer(),
3645 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3653 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3646 user_id = Column(
3654 user_id = Column(
3647 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3655 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3648 _reasons = Column(
3656 _reasons = Column(
3649 'reason', MutationList.as_mutable(
3657 'reason', MutationList.as_mutable(
3650 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3658 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3651 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3659 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3652 user = relationship('User')
3660 user = relationship('User')
3653 pull_request = relationship('PullRequest')
3661 pull_request = relationship('PullRequest')
3654
3662
3655
3663
3656 class Notification(Base, BaseModel):
3664 class Notification(Base, BaseModel):
3657 __tablename__ = 'notifications'
3665 __tablename__ = 'notifications'
3658 __table_args__ = (
3666 __table_args__ = (
3659 Index('notification_type_idx', 'type'),
3667 Index('notification_type_idx', 'type'),
3660 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3668 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3661 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3669 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3662 )
3670 )
3663
3671
3664 TYPE_CHANGESET_COMMENT = u'cs_comment'
3672 TYPE_CHANGESET_COMMENT = u'cs_comment'
3665 TYPE_MESSAGE = u'message'
3673 TYPE_MESSAGE = u'message'
3666 TYPE_MENTION = u'mention'
3674 TYPE_MENTION = u'mention'
3667 TYPE_REGISTRATION = u'registration'
3675 TYPE_REGISTRATION = u'registration'
3668 TYPE_PULL_REQUEST = u'pull_request'
3676 TYPE_PULL_REQUEST = u'pull_request'
3669 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3677 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3670
3678
3671 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3679 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3672 subject = Column('subject', Unicode(512), nullable=True)
3680 subject = Column('subject', Unicode(512), nullable=True)
3673 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3681 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3674 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3682 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3675 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3683 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3676 type_ = Column('type', Unicode(255))
3684 type_ = Column('type', Unicode(255))
3677
3685
3678 created_by_user = relationship('User')
3686 created_by_user = relationship('User')
3679 notifications_to_users = relationship('UserNotification', lazy='joined',
3687 notifications_to_users = relationship('UserNotification', lazy='joined',
3680 cascade="all, delete, delete-orphan")
3688 cascade="all, delete, delete-orphan")
3681
3689
3682 @property
3690 @property
3683 def recipients(self):
3691 def recipients(self):
3684 return [x.user for x in UserNotification.query()\
3692 return [x.user for x in UserNotification.query()\
3685 .filter(UserNotification.notification == self)\
3693 .filter(UserNotification.notification == self)\
3686 .order_by(UserNotification.user_id.asc()).all()]
3694 .order_by(UserNotification.user_id.asc()).all()]
3687
3695
3688 @classmethod
3696 @classmethod
3689 def create(cls, created_by, subject, body, recipients, type_=None):
3697 def create(cls, created_by, subject, body, recipients, type_=None):
3690 if type_ is None:
3698 if type_ is None:
3691 type_ = Notification.TYPE_MESSAGE
3699 type_ = Notification.TYPE_MESSAGE
3692
3700
3693 notification = cls()
3701 notification = cls()
3694 notification.created_by_user = created_by
3702 notification.created_by_user = created_by
3695 notification.subject = subject
3703 notification.subject = subject
3696 notification.body = body
3704 notification.body = body
3697 notification.type_ = type_
3705 notification.type_ = type_
3698 notification.created_on = datetime.datetime.now()
3706 notification.created_on = datetime.datetime.now()
3699
3707
3700 for u in recipients:
3708 for u in recipients:
3701 assoc = UserNotification()
3709 assoc = UserNotification()
3702 assoc.notification = notification
3710 assoc.notification = notification
3703
3711
3704 # if created_by is inside recipients mark his notification
3712 # if created_by is inside recipients mark his notification
3705 # as read
3713 # as read
3706 if u.user_id == created_by.user_id:
3714 if u.user_id == created_by.user_id:
3707 assoc.read = True
3715 assoc.read = True
3708
3716
3709 u.notifications.append(assoc)
3717 u.notifications.append(assoc)
3710 Session().add(notification)
3718 Session().add(notification)
3711
3719
3712 return notification
3720 return notification
3713
3721
3714
3722
3715 class UserNotification(Base, BaseModel):
3723 class UserNotification(Base, BaseModel):
3716 __tablename__ = 'user_to_notification'
3724 __tablename__ = 'user_to_notification'
3717 __table_args__ = (
3725 __table_args__ = (
3718 UniqueConstraint('user_id', 'notification_id'),
3726 UniqueConstraint('user_id', 'notification_id'),
3719 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3727 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3720 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3728 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3721 )
3729 )
3722 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3730 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3723 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3731 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3724 read = Column('read', Boolean, default=False)
3732 read = Column('read', Boolean, default=False)
3725 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3733 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3726
3734
3727 user = relationship('User', lazy="joined")
3735 user = relationship('User', lazy="joined")
3728 notification = relationship('Notification', lazy="joined",
3736 notification = relationship('Notification', lazy="joined",
3729 order_by=lambda: Notification.created_on.desc(),)
3737 order_by=lambda: Notification.created_on.desc(),)
3730
3738
3731 def mark_as_read(self):
3739 def mark_as_read(self):
3732 self.read = True
3740 self.read = True
3733 Session().add(self)
3741 Session().add(self)
3734
3742
3735
3743
3736 class Gist(Base, BaseModel):
3744 class Gist(Base, BaseModel):
3737 __tablename__ = 'gists'
3745 __tablename__ = 'gists'
3738 __table_args__ = (
3746 __table_args__ = (
3739 Index('g_gist_access_id_idx', 'gist_access_id'),
3747 Index('g_gist_access_id_idx', 'gist_access_id'),
3740 Index('g_created_on_idx', 'created_on'),
3748 Index('g_created_on_idx', 'created_on'),
3741 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3749 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3742 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3750 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3743 )
3751 )
3744 GIST_PUBLIC = u'public'
3752 GIST_PUBLIC = u'public'
3745 GIST_PRIVATE = u'private'
3753 GIST_PRIVATE = u'private'
3746 DEFAULT_FILENAME = u'gistfile1.txt'
3754 DEFAULT_FILENAME = u'gistfile1.txt'
3747
3755
3748 ACL_LEVEL_PUBLIC = u'acl_public'
3756 ACL_LEVEL_PUBLIC = u'acl_public'
3749 ACL_LEVEL_PRIVATE = u'acl_private'
3757 ACL_LEVEL_PRIVATE = u'acl_private'
3750
3758
3751 gist_id = Column('gist_id', Integer(), primary_key=True)
3759 gist_id = Column('gist_id', Integer(), primary_key=True)
3752 gist_access_id = Column('gist_access_id', Unicode(250))
3760 gist_access_id = Column('gist_access_id', Unicode(250))
3753 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3761 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3754 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3762 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3755 gist_expires = Column('gist_expires', Float(53), nullable=False)
3763 gist_expires = Column('gist_expires', Float(53), nullable=False)
3756 gist_type = Column('gist_type', Unicode(128), nullable=False)
3764 gist_type = Column('gist_type', Unicode(128), nullable=False)
3757 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3765 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3758 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3766 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3759 acl_level = Column('acl_level', Unicode(128), nullable=True)
3767 acl_level = Column('acl_level', Unicode(128), nullable=True)
3760
3768
3761 owner = relationship('User')
3769 owner = relationship('User')
3762
3770
3763 def __repr__(self):
3771 def __repr__(self):
3764 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3772 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3765
3773
3766 @hybrid_property
3774 @hybrid_property
3767 def description_safe(self):
3775 def description_safe(self):
3768 from rhodecode.lib import helpers as h
3776 from rhodecode.lib import helpers as h
3769 return h.escape(self.gist_description)
3777 return h.escape(self.gist_description)
3770
3778
3771 @classmethod
3779 @classmethod
3772 def get_or_404(cls, id_):
3780 def get_or_404(cls, id_):
3773 from pyramid.httpexceptions import HTTPNotFound
3781 from pyramid.httpexceptions import HTTPNotFound
3774
3782
3775 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3783 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3776 if not res:
3784 if not res:
3777 raise HTTPNotFound()
3785 raise HTTPNotFound()
3778 return res
3786 return res
3779
3787
3780 @classmethod
3788 @classmethod
3781 def get_by_access_id(cls, gist_access_id):
3789 def get_by_access_id(cls, gist_access_id):
3782 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3790 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3783
3791
3784 def gist_url(self):
3792 def gist_url(self):
3785 from rhodecode.model.gist import GistModel
3793 from rhodecode.model.gist import GistModel
3786 return GistModel().get_url(self)
3794 return GistModel().get_url(self)
3787
3795
3788 @classmethod
3796 @classmethod
3789 def base_path(cls):
3797 def base_path(cls):
3790 """
3798 """
3791 Returns base path when all gists are stored
3799 Returns base path when all gists are stored
3792
3800
3793 :param cls:
3801 :param cls:
3794 """
3802 """
3795 from rhodecode.model.gist import GIST_STORE_LOC
3803 from rhodecode.model.gist import GIST_STORE_LOC
3796 q = Session().query(RhodeCodeUi)\
3804 q = Session().query(RhodeCodeUi)\
3797 .filter(RhodeCodeUi.ui_key == URL_SEP)
3805 .filter(RhodeCodeUi.ui_key == URL_SEP)
3798 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3806 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3799 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3807 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3800
3808
3801 def get_api_data(self):
3809 def get_api_data(self):
3802 """
3810 """
3803 Common function for generating gist related data for API
3811 Common function for generating gist related data for API
3804 """
3812 """
3805 gist = self
3813 gist = self
3806 data = {
3814 data = {
3807 'gist_id': gist.gist_id,
3815 'gist_id': gist.gist_id,
3808 'type': gist.gist_type,
3816 'type': gist.gist_type,
3809 'access_id': gist.gist_access_id,
3817 'access_id': gist.gist_access_id,
3810 'description': gist.gist_description,
3818 'description': gist.gist_description,
3811 'url': gist.gist_url(),
3819 'url': gist.gist_url(),
3812 'expires': gist.gist_expires,
3820 'expires': gist.gist_expires,
3813 'created_on': gist.created_on,
3821 'created_on': gist.created_on,
3814 'modified_at': gist.modified_at,
3822 'modified_at': gist.modified_at,
3815 'content': None,
3823 'content': None,
3816 'acl_level': gist.acl_level,
3824 'acl_level': gist.acl_level,
3817 }
3825 }
3818 return data
3826 return data
3819
3827
3820 def __json__(self):
3828 def __json__(self):
3821 data = dict(
3829 data = dict(
3822 )
3830 )
3823 data.update(self.get_api_data())
3831 data.update(self.get_api_data())
3824 return data
3832 return data
3825 # SCM functions
3833 # SCM functions
3826
3834
3827 def scm_instance(self, **kwargs):
3835 def scm_instance(self, **kwargs):
3828 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3836 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3829 return get_vcs_instance(
3837 return get_vcs_instance(
3830 repo_path=safe_str(full_repo_path), create=False)
3838 repo_path=safe_str(full_repo_path), create=False)
3831
3839
3832
3840
3833 class ExternalIdentity(Base, BaseModel):
3841 class ExternalIdentity(Base, BaseModel):
3834 __tablename__ = 'external_identities'
3842 __tablename__ = 'external_identities'
3835 __table_args__ = (
3843 __table_args__ = (
3836 Index('local_user_id_idx', 'local_user_id'),
3844 Index('local_user_id_idx', 'local_user_id'),
3837 Index('external_id_idx', 'external_id'),
3845 Index('external_id_idx', 'external_id'),
3838 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3846 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3839 'mysql_charset': 'utf8'})
3847 'mysql_charset': 'utf8'})
3840
3848
3841 external_id = Column('external_id', Unicode(255), default=u'',
3849 external_id = Column('external_id', Unicode(255), default=u'',
3842 primary_key=True)
3850 primary_key=True)
3843 external_username = Column('external_username', Unicode(1024), default=u'')
3851 external_username = Column('external_username', Unicode(1024), default=u'')
3844 local_user_id = Column('local_user_id', Integer(),
3852 local_user_id = Column('local_user_id', Integer(),
3845 ForeignKey('users.user_id'), primary_key=True)
3853 ForeignKey('users.user_id'), primary_key=True)
3846 provider_name = Column('provider_name', Unicode(255), default=u'',
3854 provider_name = Column('provider_name', Unicode(255), default=u'',
3847 primary_key=True)
3855 primary_key=True)
3848 access_token = Column('access_token', String(1024), default=u'')
3856 access_token = Column('access_token', String(1024), default=u'')
3849 alt_token = Column('alt_token', String(1024), default=u'')
3857 alt_token = Column('alt_token', String(1024), default=u'')
3850 token_secret = Column('token_secret', String(1024), default=u'')
3858 token_secret = Column('token_secret', String(1024), default=u'')
3851
3859
3852 @classmethod
3860 @classmethod
3853 def by_external_id_and_provider(cls, external_id, provider_name,
3861 def by_external_id_and_provider(cls, external_id, provider_name,
3854 local_user_id=None):
3862 local_user_id=None):
3855 """
3863 """
3856 Returns ExternalIdentity instance based on search params
3864 Returns ExternalIdentity instance based on search params
3857
3865
3858 :param external_id:
3866 :param external_id:
3859 :param provider_name:
3867 :param provider_name:
3860 :return: ExternalIdentity
3868 :return: ExternalIdentity
3861 """
3869 """
3862 query = cls.query()
3870 query = cls.query()
3863 query = query.filter(cls.external_id == external_id)
3871 query = query.filter(cls.external_id == external_id)
3864 query = query.filter(cls.provider_name == provider_name)
3872 query = query.filter(cls.provider_name == provider_name)
3865 if local_user_id:
3873 if local_user_id:
3866 query = query.filter(cls.local_user_id == local_user_id)
3874 query = query.filter(cls.local_user_id == local_user_id)
3867 return query.first()
3875 return query.first()
3868
3876
3869 @classmethod
3877 @classmethod
3870 def user_by_external_id_and_provider(cls, external_id, provider_name):
3878 def user_by_external_id_and_provider(cls, external_id, provider_name):
3871 """
3879 """
3872 Returns User instance based on search params
3880 Returns User instance based on search params
3873
3881
3874 :param external_id:
3882 :param external_id:
3875 :param provider_name:
3883 :param provider_name:
3876 :return: User
3884 :return: User
3877 """
3885 """
3878 query = User.query()
3886 query = User.query()
3879 query = query.filter(cls.external_id == external_id)
3887 query = query.filter(cls.external_id == external_id)
3880 query = query.filter(cls.provider_name == provider_name)
3888 query = query.filter(cls.provider_name == provider_name)
3881 query = query.filter(User.user_id == cls.local_user_id)
3889 query = query.filter(User.user_id == cls.local_user_id)
3882 return query.first()
3890 return query.first()
3883
3891
3884 @classmethod
3892 @classmethod
3885 def by_local_user_id(cls, local_user_id):
3893 def by_local_user_id(cls, local_user_id):
3886 """
3894 """
3887 Returns all tokens for user
3895 Returns all tokens for user
3888
3896
3889 :param local_user_id:
3897 :param local_user_id:
3890 :return: ExternalIdentity
3898 :return: ExternalIdentity
3891 """
3899 """
3892 query = cls.query()
3900 query = cls.query()
3893 query = query.filter(cls.local_user_id == local_user_id)
3901 query = query.filter(cls.local_user_id == local_user_id)
3894 return query
3902 return query
3895
3903
3896
3904
3897 class Integration(Base, BaseModel):
3905 class Integration(Base, BaseModel):
3898 __tablename__ = 'integrations'
3906 __tablename__ = 'integrations'
3899 __table_args__ = (
3907 __table_args__ = (
3900 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3908 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3901 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3909 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3902 )
3910 )
3903
3911
3904 integration_id = Column('integration_id', Integer(), primary_key=True)
3912 integration_id = Column('integration_id', Integer(), primary_key=True)
3905 integration_type = Column('integration_type', String(255))
3913 integration_type = Column('integration_type', String(255))
3906 enabled = Column('enabled', Boolean(), nullable=False)
3914 enabled = Column('enabled', Boolean(), nullable=False)
3907 name = Column('name', String(255), nullable=False)
3915 name = Column('name', String(255), nullable=False)
3908 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3916 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3909 default=False)
3917 default=False)
3910
3918
3911 settings = Column(
3919 settings = Column(
3912 'settings_json', MutationObj.as_mutable(
3920 'settings_json', MutationObj.as_mutable(
3913 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3921 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3914 repo_id = Column(
3922 repo_id = Column(
3915 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3923 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3916 nullable=True, unique=None, default=None)
3924 nullable=True, unique=None, default=None)
3917 repo = relationship('Repository', lazy='joined')
3925 repo = relationship('Repository', lazy='joined')
3918
3926
3919 repo_group_id = Column(
3927 repo_group_id = Column(
3920 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3928 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3921 nullable=True, unique=None, default=None)
3929 nullable=True, unique=None, default=None)
3922 repo_group = relationship('RepoGroup', lazy='joined')
3930 repo_group = relationship('RepoGroup', lazy='joined')
3923
3931
3924 @property
3932 @property
3925 def scope(self):
3933 def scope(self):
3926 if self.repo:
3934 if self.repo:
3927 return repr(self.repo)
3935 return repr(self.repo)
3928 if self.repo_group:
3936 if self.repo_group:
3929 if self.child_repos_only:
3937 if self.child_repos_only:
3930 return repr(self.repo_group) + ' (child repos only)'
3938 return repr(self.repo_group) + ' (child repos only)'
3931 else:
3939 else:
3932 return repr(self.repo_group) + ' (recursive)'
3940 return repr(self.repo_group) + ' (recursive)'
3933 if self.child_repos_only:
3941 if self.child_repos_only:
3934 return 'root_repos'
3942 return 'root_repos'
3935 return 'global'
3943 return 'global'
3936
3944
3937 def __repr__(self):
3945 def __repr__(self):
3938 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3946 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3939
3947
3940
3948
3941 class RepoReviewRuleUser(Base, BaseModel):
3949 class RepoReviewRuleUser(Base, BaseModel):
3942 __tablename__ = 'repo_review_rules_users'
3950 __tablename__ = 'repo_review_rules_users'
3943 __table_args__ = (
3951 __table_args__ = (
3944 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3952 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3945 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3953 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3946 )
3954 )
3947 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3955 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3948 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3956 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3949 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3957 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3950 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3958 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3951 user = relationship('User')
3959 user = relationship('User')
3952
3960
3953 def rule_data(self):
3961 def rule_data(self):
3954 return {
3962 return {
3955 'mandatory': self.mandatory
3963 'mandatory': self.mandatory
3956 }
3964 }
3957
3965
3958
3966
3959 class RepoReviewRuleUserGroup(Base, BaseModel):
3967 class RepoReviewRuleUserGroup(Base, BaseModel):
3960 __tablename__ = 'repo_review_rules_users_groups'
3968 __tablename__ = 'repo_review_rules_users_groups'
3961 __table_args__ = (
3969 __table_args__ = (
3962 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3970 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3963 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3971 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3964 )
3972 )
3965 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3973 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3966 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3974 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3967 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3975 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3968 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3976 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3969 users_group = relationship('UserGroup')
3977 users_group = relationship('UserGroup')
3970
3978
3971 def rule_data(self):
3979 def rule_data(self):
3972 return {
3980 return {
3973 'mandatory': self.mandatory
3981 'mandatory': self.mandatory
3974 }
3982 }
3975
3983
3976
3984
3977 class RepoReviewRule(Base, BaseModel):
3985 class RepoReviewRule(Base, BaseModel):
3978 __tablename__ = 'repo_review_rules'
3986 __tablename__ = 'repo_review_rules'
3979 __table_args__ = (
3987 __table_args__ = (
3980 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3988 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3981 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3989 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3982 )
3990 )
3983
3991
3984 repo_review_rule_id = Column(
3992 repo_review_rule_id = Column(
3985 'repo_review_rule_id', Integer(), primary_key=True)
3993 'repo_review_rule_id', Integer(), primary_key=True)
3986 repo_id = Column(
3994 repo_id = Column(
3987 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3995 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3988 repo = relationship('Repository', backref='review_rules')
3996 repo = relationship('Repository', backref='review_rules')
3989
3997
3990 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3998 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3991 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3999 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3992
4000
3993 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4001 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3994 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4002 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3995 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4003 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
3996 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4004 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3997
4005
3998 rule_users = relationship('RepoReviewRuleUser')
4006 rule_users = relationship('RepoReviewRuleUser')
3999 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4007 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4000
4008
4001 @hybrid_property
4009 @hybrid_property
4002 def branch_pattern(self):
4010 def branch_pattern(self):
4003 return self._branch_pattern or '*'
4011 return self._branch_pattern or '*'
4004
4012
4005 def _validate_glob(self, value):
4013 def _validate_glob(self, value):
4006 re.compile('^' + glob2re(value) + '$')
4014 re.compile('^' + glob2re(value) + '$')
4007
4015
4008 @branch_pattern.setter
4016 @branch_pattern.setter
4009 def branch_pattern(self, value):
4017 def branch_pattern(self, value):
4010 self._validate_glob(value)
4018 self._validate_glob(value)
4011 self._branch_pattern = value or '*'
4019 self._branch_pattern = value or '*'
4012
4020
4013 @hybrid_property
4021 @hybrid_property
4014 def file_pattern(self):
4022 def file_pattern(self):
4015 return self._file_pattern or '*'
4023 return self._file_pattern or '*'
4016
4024
4017 @file_pattern.setter
4025 @file_pattern.setter
4018 def file_pattern(self, value):
4026 def file_pattern(self, value):
4019 self._validate_glob(value)
4027 self._validate_glob(value)
4020 self._file_pattern = value or '*'
4028 self._file_pattern = value or '*'
4021
4029
4022 def matches(self, branch, files_changed):
4030 def matches(self, branch, files_changed):
4023 """
4031 """
4024 Check if this review rule matches a branch/files in a pull request
4032 Check if this review rule matches a branch/files in a pull request
4025
4033
4026 :param branch: branch name for the commit
4034 :param branch: branch name for the commit
4027 :param files_changed: list of file paths changed in the pull request
4035 :param files_changed: list of file paths changed in the pull request
4028 """
4036 """
4029
4037
4030 branch = branch or ''
4038 branch = branch or ''
4031 files_changed = files_changed or []
4039 files_changed = files_changed or []
4032
4040
4033 branch_matches = True
4041 branch_matches = True
4034 if branch:
4042 if branch:
4035 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4043 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4036 branch_matches = bool(branch_regex.search(branch))
4044 branch_matches = bool(branch_regex.search(branch))
4037
4045
4038 files_matches = True
4046 files_matches = True
4039 if self.file_pattern != '*':
4047 if self.file_pattern != '*':
4040 files_matches = False
4048 files_matches = False
4041 file_regex = re.compile(glob2re(self.file_pattern))
4049 file_regex = re.compile(glob2re(self.file_pattern))
4042 for filename in files_changed:
4050 for filename in files_changed:
4043 if file_regex.search(filename):
4051 if file_regex.search(filename):
4044 files_matches = True
4052 files_matches = True
4045 break
4053 break
4046
4054
4047 return branch_matches and files_matches
4055 return branch_matches and files_matches
4048
4056
4049 @property
4057 @property
4050 def review_users(self):
4058 def review_users(self):
4051 """ Returns the users which this rule applies to """
4059 """ Returns the users which this rule applies to """
4052
4060
4053 users = collections.OrderedDict()
4061 users = collections.OrderedDict()
4054
4062
4055 for rule_user in self.rule_users:
4063 for rule_user in self.rule_users:
4056 if rule_user.user.active:
4064 if rule_user.user.active:
4057 if rule_user.user not in users:
4065 if rule_user.user not in users:
4058 users[rule_user.user.username] = {
4066 users[rule_user.user.username] = {
4059 'user': rule_user.user,
4067 'user': rule_user.user,
4060 'source': 'user',
4068 'source': 'user',
4061 'source_data': {},
4069 'source_data': {},
4062 'data': rule_user.rule_data()
4070 'data': rule_user.rule_data()
4063 }
4071 }
4064
4072
4065 for rule_user_group in self.rule_user_groups:
4073 for rule_user_group in self.rule_user_groups:
4066 source_data = {
4074 source_data = {
4067 'name': rule_user_group.users_group.users_group_name,
4075 'name': rule_user_group.users_group.users_group_name,
4068 'members': len(rule_user_group.users_group.members)
4076 'members': len(rule_user_group.users_group.members)
4069 }
4077 }
4070 for member in rule_user_group.users_group.members:
4078 for member in rule_user_group.users_group.members:
4071 if member.user.active:
4079 if member.user.active:
4072 users[member.user.username] = {
4080 users[member.user.username] = {
4073 'user': member.user,
4081 'user': member.user,
4074 'source': 'user_group',
4082 'source': 'user_group',
4075 'source_data': source_data,
4083 'source_data': source_data,
4076 'data': rule_user_group.rule_data()
4084 'data': rule_user_group.rule_data()
4077 }
4085 }
4078
4086
4079 return users
4087 return users
4080
4088
4081 def __repr__(self):
4089 def __repr__(self):
4082 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4090 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4083 self.repo_review_rule_id, self.repo)
4091 self.repo_review_rule_id, self.repo)
4084
4092
4085
4093
4086 class DbMigrateVersion(Base, BaseModel):
4094 class DbMigrateVersion(Base, BaseModel):
4087 __tablename__ = 'db_migrate_version'
4095 __tablename__ = 'db_migrate_version'
4088 __table_args__ = (
4096 __table_args__ = (
4089 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4097 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4090 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4098 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4091 )
4099 )
4092 repository_id = Column('repository_id', String(250), primary_key=True)
4100 repository_id = Column('repository_id', String(250), primary_key=True)
4093 repository_path = Column('repository_path', Text)
4101 repository_path = Column('repository_path', Text)
4094 version = Column('version', Integer)
4102 version = Column('version', Integer)
4095
4103
4096
4104
4097 class DbSession(Base, BaseModel):
4105 class DbSession(Base, BaseModel):
4098 __tablename__ = 'db_session'
4106 __tablename__ = 'db_session'
4099 __table_args__ = (
4107 __table_args__ = (
4100 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4108 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4101 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4109 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4102 )
4110 )
4103
4111
4104 def __repr__(self):
4112 def __repr__(self):
4105 return '<DB:DbSession({})>'.format(self.id)
4113 return '<DB:DbSession({})>'.format(self.id)
4106
4114
4107 id = Column('id', Integer())
4115 id = Column('id', Integer())
4108 namespace = Column('namespace', String(255), primary_key=True)
4116 namespace = Column('namespace', String(255), primary_key=True)
4109 accessed = Column('accessed', DateTime, nullable=False)
4117 accessed = Column('accessed', DateTime, nullable=False)
4110 created = Column('created', DateTime, nullable=False)
4118 created = Column('created', DateTime, nullable=False)
4111 data = Column('data', PickleType, nullable=False)
4119 data = Column('data', PickleType, nullable=False)
@@ -1,1551 +1,1551 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
35 from sqlalchemy import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository)
55 PullRequestVersion, ChangesetComment, Repository)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = namedtuple('UpdateResponse', [
68 UpdateResponse = namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = 3
77 DIFF_CONTEXT = 3
78
78
79 MERGE_STATUS_MESSAGES = {
79 MERGE_STATUS_MESSAGES = {
80 MergeFailureReason.NONE: lazy_ugettext(
80 MergeFailureReason.NONE: lazy_ugettext(
81 'This pull request can be automatically merged.'),
81 'This pull request can be automatically merged.'),
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 'This pull request cannot be merged because of an unhandled'
83 'This pull request cannot be merged because of an unhandled'
84 ' exception.'),
84 ' exception.'),
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 'This pull request cannot be merged because of merge conflicts.'),
86 'This pull request cannot be merged because of merge conflicts.'),
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 'This pull request could not be merged because push to target'
88 'This pull request could not be merged because push to target'
89 ' failed.'),
89 ' failed.'),
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 'This pull request cannot be merged because the target is not a'
91 'This pull request cannot be merged because the target is not a'
92 ' head.'),
92 ' head.'),
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 'This pull request cannot be merged because the source contains'
94 'This pull request cannot be merged because the source contains'
95 ' more branches than the target.'),
95 ' more branches than the target.'),
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 'This pull request cannot be merged because the target has'
97 'This pull request cannot be merged because the target has'
98 ' multiple heads.'),
98 ' multiple heads.'),
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 'This pull request cannot be merged because the target repository'
100 'This pull request cannot be merged because the target repository'
101 ' is locked.'),
101 ' is locked.'),
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 'This pull request cannot be merged because the target or the '
103 'This pull request cannot be merged because the target or the '
104 'source reference is missing.'),
104 'source reference is missing.'),
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 'This pull request cannot be merged because the target '
106 'This pull request cannot be merged because the target '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 'This pull request cannot be merged because the source '
109 'This pull request cannot be merged because the source '
110 'reference is missing.'),
110 'reference is missing.'),
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 'This pull request cannot be merged because of conflicts related '
112 'This pull request cannot be merged because of conflicts related '
113 'to sub repositories.'),
113 'to sub repositories.'),
114 }
114 }
115
115
116 UPDATE_STATUS_MESSAGES = {
116 UPDATE_STATUS_MESSAGES = {
117 UpdateFailureReason.NONE: lazy_ugettext(
117 UpdateFailureReason.NONE: lazy_ugettext(
118 'Pull request update successful.'),
118 'Pull request update successful.'),
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 'Pull request update failed because of an unknown error.'),
120 'Pull request update failed because of an unknown error.'),
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 'No update needed because the source and target have not changed.'),
122 'No update needed because the source and target have not changed.'),
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 'Pull request cannot be updated because the reference type is '
124 'Pull request cannot be updated because the reference type is '
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 'This pull request cannot be updated because the target '
127 'This pull request cannot be updated because the target '
128 'reference is missing.'),
128 'reference is missing.'),
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 'This pull request cannot be updated because the source '
130 'This pull request cannot be updated because the source '
131 'reference is missing.'),
131 'reference is missing.'),
132 }
132 }
133
133
134 def __get_pull_request(self, pull_request):
134 def __get_pull_request(self, pull_request):
135 return self._get_instance((
135 return self._get_instance((
136 PullRequest, PullRequestVersion), pull_request)
136 PullRequest, PullRequestVersion), pull_request)
137
137
138 def _check_perms(self, perms, pull_request, user, api=False):
138 def _check_perms(self, perms, pull_request, user, api=False):
139 if not api:
139 if not api:
140 return h.HasRepoPermissionAny(*perms)(
140 return h.HasRepoPermissionAny(*perms)(
141 user=user, repo_name=pull_request.target_repo.repo_name)
141 user=user, repo_name=pull_request.target_repo.repo_name)
142 else:
142 else:
143 return h.HasRepoPermissionAnyApi(*perms)(
143 return h.HasRepoPermissionAnyApi(*perms)(
144 user=user, repo_name=pull_request.target_repo.repo_name)
144 user=user, repo_name=pull_request.target_repo.repo_name)
145
145
146 def check_user_read(self, pull_request, user, api=False):
146 def check_user_read(self, pull_request, user, api=False):
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 return self._check_perms(_perms, pull_request, user, api)
148 return self._check_perms(_perms, pull_request, user, api)
149
149
150 def check_user_merge(self, pull_request, user, api=False):
150 def check_user_merge(self, pull_request, user, api=False):
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 return self._check_perms(_perms, pull_request, user, api)
152 return self._check_perms(_perms, pull_request, user, api)
153
153
154 def check_user_update(self, pull_request, user, api=False):
154 def check_user_update(self, pull_request, user, api=False):
155 owner = user.user_id == pull_request.user_id
155 owner = user.user_id == pull_request.user_id
156 return self.check_user_merge(pull_request, user, api) or owner
156 return self.check_user_merge(pull_request, user, api) or owner
157
157
158 def check_user_delete(self, pull_request, user):
158 def check_user_delete(self, pull_request, user):
159 owner = user.user_id == pull_request.user_id
159 owner = user.user_id == pull_request.user_id
160 _perms = ('repository.admin',)
160 _perms = ('repository.admin',)
161 return self._check_perms(_perms, pull_request, user) or owner
161 return self._check_perms(_perms, pull_request, user) or owner
162
162
163 def check_user_change_status(self, pull_request, user, api=False):
163 def check_user_change_status(self, pull_request, user, api=False):
164 reviewer = user.user_id in [x.user_id for x in
164 reviewer = user.user_id in [x.user_id for x in
165 pull_request.reviewers]
165 pull_request.reviewers]
166 return self.check_user_update(pull_request, user, api) or reviewer
166 return self.check_user_update(pull_request, user, api) or reviewer
167
167
168 def get(self, pull_request):
168 def get(self, pull_request):
169 return self.__get_pull_request(pull_request)
169 return self.__get_pull_request(pull_request)
170
170
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 opened_by=None, order_by=None,
172 opened_by=None, order_by=None,
173 order_dir='desc'):
173 order_dir='desc'):
174 repo = None
174 repo = None
175 if repo_name:
175 if repo_name:
176 repo = self._get_repo(repo_name)
176 repo = self._get_repo(repo_name)
177
177
178 q = PullRequest.query()
178 q = PullRequest.query()
179
179
180 # source or target
180 # source or target
181 if repo and source:
181 if repo and source:
182 q = q.filter(PullRequest.source_repo == repo)
182 q = q.filter(PullRequest.source_repo == repo)
183 elif repo:
183 elif repo:
184 q = q.filter(PullRequest.target_repo == repo)
184 q = q.filter(PullRequest.target_repo == repo)
185
185
186 # closed,opened
186 # closed,opened
187 if statuses:
187 if statuses:
188 q = q.filter(PullRequest.status.in_(statuses))
188 q = q.filter(PullRequest.status.in_(statuses))
189
189
190 # opened by filter
190 # opened by filter
191 if opened_by:
191 if opened_by:
192 q = q.filter(PullRequest.user_id.in_(opened_by))
192 q = q.filter(PullRequest.user_id.in_(opened_by))
193
193
194 if order_by:
194 if order_by:
195 order_map = {
195 order_map = {
196 'name_raw': PullRequest.pull_request_id,
196 'name_raw': PullRequest.pull_request_id,
197 'title': PullRequest.title,
197 'title': PullRequest.title,
198 'updated_on_raw': PullRequest.updated_on,
198 'updated_on_raw': PullRequest.updated_on,
199 'target_repo': PullRequest.target_repo_id
199 'target_repo': PullRequest.target_repo_id
200 }
200 }
201 if order_dir == 'asc':
201 if order_dir == 'asc':
202 q = q.order_by(order_map[order_by].asc())
202 q = q.order_by(order_map[order_by].asc())
203 else:
203 else:
204 q = q.order_by(order_map[order_by].desc())
204 q = q.order_by(order_map[order_by].desc())
205
205
206 return q
206 return q
207
207
208 def count_all(self, repo_name, source=False, statuses=None,
208 def count_all(self, repo_name, source=False, statuses=None,
209 opened_by=None):
209 opened_by=None):
210 """
210 """
211 Count the number of pull requests for a specific repository.
211 Count the number of pull requests for a specific repository.
212
212
213 :param repo_name: target or source repo
213 :param repo_name: target or source repo
214 :param source: boolean flag to specify if repo_name refers to source
214 :param source: boolean flag to specify if repo_name refers to source
215 :param statuses: list of pull request statuses
215 :param statuses: list of pull request statuses
216 :param opened_by: author user of the pull request
216 :param opened_by: author user of the pull request
217 :returns: int number of pull requests
217 :returns: int number of pull requests
218 """
218 """
219 q = self._prepare_get_all_query(
219 q = self._prepare_get_all_query(
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221
221
222 return q.count()
222 return q.count()
223
223
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 offset=0, length=None, order_by=None, order_dir='desc'):
225 offset=0, length=None, order_by=None, order_dir='desc'):
226 """
226 """
227 Get all pull requests for a specific repository.
227 Get all pull requests for a specific repository.
228
228
229 :param repo_name: target or source repo
229 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
230 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
231 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
232 :param opened_by: author user of the pull request
233 :param offset: pagination offset
233 :param offset: pagination offset
234 :param length: length of returned list
234 :param length: length of returned list
235 :param order_by: order of the returned list
235 :param order_by: order of the returned list
236 :param order_dir: 'asc' or 'desc' ordering direction
236 :param order_dir: 'asc' or 'desc' ordering direction
237 :returns: list of pull requests
237 :returns: list of pull requests
238 """
238 """
239 q = self._prepare_get_all_query(
239 q = self._prepare_get_all_query(
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 order_by=order_by, order_dir=order_dir)
241 order_by=order_by, order_dir=order_dir)
242
242
243 if length:
243 if length:
244 pull_requests = q.limit(length).offset(offset).all()
244 pull_requests = q.limit(length).offset(offset).all()
245 else:
245 else:
246 pull_requests = q.all()
246 pull_requests = q.all()
247
247
248 return pull_requests
248 return pull_requests
249
249
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None):
251 opened_by=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review.
254 awaiting review.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :returns: int number of pull requests
260 :returns: int number of pull requests
261 """
261 """
262 pull_requests = self.get_awaiting_review(
262 pull_requests = self.get_awaiting_review(
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264
264
265 return len(pull_requests)
265 return len(pull_requests)
266
266
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 opened_by=None, offset=0, length=None,
268 opened_by=None, offset=0, length=None,
269 order_by=None, order_dir='desc'):
269 order_by=None, order_dir='desc'):
270 """
270 """
271 Get all pull requests for a specific repository that are awaiting
271 Get all pull requests for a specific repository that are awaiting
272 review.
272 review.
273
273
274 :param repo_name: target or source repo
274 :param repo_name: target or source repo
275 :param source: boolean flag to specify if repo_name refers to source
275 :param source: boolean flag to specify if repo_name refers to source
276 :param statuses: list of pull request statuses
276 :param statuses: list of pull request statuses
277 :param opened_by: author user of the pull request
277 :param opened_by: author user of the pull request
278 :param offset: pagination offset
278 :param offset: pagination offset
279 :param length: length of returned list
279 :param length: length of returned list
280 :param order_by: order of the returned list
280 :param order_by: order of the returned list
281 :param order_dir: 'asc' or 'desc' ordering direction
281 :param order_dir: 'asc' or 'desc' ordering direction
282 :returns: list of pull requests
282 :returns: list of pull requests
283 """
283 """
284 pull_requests = self.get_all(
284 pull_requests = self.get_all(
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 order_by=order_by, order_dir=order_dir)
286 order_by=order_by, order_dir=order_dir)
287
287
288 _filtered_pull_requests = []
288 _filtered_pull_requests = []
289 for pr in pull_requests:
289 for pr in pull_requests:
290 status = pr.calculated_review_status()
290 status = pr.calculated_review_status()
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 _filtered_pull_requests.append(pr)
293 _filtered_pull_requests.append(pr)
294 if length:
294 if length:
295 return _filtered_pull_requests[offset:offset+length]
295 return _filtered_pull_requests[offset:offset+length]
296 else:
296 else:
297 return _filtered_pull_requests
297 return _filtered_pull_requests
298
298
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 opened_by=None, user_id=None):
300 opened_by=None, user_id=None):
301 """
301 """
302 Count the number of pull requests for a specific repository that are
302 Count the number of pull requests for a specific repository that are
303 awaiting review from a specific user.
303 awaiting review from a specific user.
304
304
305 :param repo_name: target or source repo
305 :param repo_name: target or source repo
306 :param source: boolean flag to specify if repo_name refers to source
306 :param source: boolean flag to specify if repo_name refers to source
307 :param statuses: list of pull request statuses
307 :param statuses: list of pull request statuses
308 :param opened_by: author user of the pull request
308 :param opened_by: author user of the pull request
309 :param user_id: reviewer user of the pull request
309 :param user_id: reviewer user of the pull request
310 :returns: int number of pull requests
310 :returns: int number of pull requests
311 """
311 """
312 pull_requests = self.get_awaiting_my_review(
312 pull_requests = self.get_awaiting_my_review(
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 user_id=user_id)
314 user_id=user_id)
315
315
316 return len(pull_requests)
316 return len(pull_requests)
317
317
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 opened_by=None, user_id=None, offset=0,
319 opened_by=None, user_id=None, offset=0,
320 length=None, order_by=None, order_dir='desc'):
320 length=None, order_by=None, order_dir='desc'):
321 """
321 """
322 Get all pull requests for a specific repository that are awaiting
322 Get all pull requests for a specific repository that are awaiting
323 review from a specific user.
323 review from a specific user.
324
324
325 :param repo_name: target or source repo
325 :param repo_name: target or source repo
326 :param source: boolean flag to specify if repo_name refers to source
326 :param source: boolean flag to specify if repo_name refers to source
327 :param statuses: list of pull request statuses
327 :param statuses: list of pull request statuses
328 :param opened_by: author user of the pull request
328 :param opened_by: author user of the pull request
329 :param user_id: reviewer user of the pull request
329 :param user_id: reviewer user of the pull request
330 :param offset: pagination offset
330 :param offset: pagination offset
331 :param length: length of returned list
331 :param length: length of returned list
332 :param order_by: order of the returned list
332 :param order_by: order of the returned list
333 :param order_dir: 'asc' or 'desc' ordering direction
333 :param order_dir: 'asc' or 'desc' ordering direction
334 :returns: list of pull requests
334 :returns: list of pull requests
335 """
335 """
336 pull_requests = self.get_all(
336 pull_requests = self.get_all(
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 order_by=order_by, order_dir=order_dir)
338 order_by=order_by, order_dir=order_dir)
339
339
340 _my = PullRequestModel().get_not_reviewed(user_id)
340 _my = PullRequestModel().get_not_reviewed(user_id)
341 my_participation = []
341 my_participation = []
342 for pr in pull_requests:
342 for pr in pull_requests:
343 if pr in _my:
343 if pr in _my:
344 my_participation.append(pr)
344 my_participation.append(pr)
345 _filtered_pull_requests = my_participation
345 _filtered_pull_requests = my_participation
346 if length:
346 if length:
347 return _filtered_pull_requests[offset:offset+length]
347 return _filtered_pull_requests[offset:offset+length]
348 else:
348 else:
349 return _filtered_pull_requests
349 return _filtered_pull_requests
350
350
351 def get_not_reviewed(self, user_id):
351 def get_not_reviewed(self, user_id):
352 return [
352 return [
353 x.pull_request for x in PullRequestReviewers.query().filter(
353 x.pull_request for x in PullRequestReviewers.query().filter(
354 PullRequestReviewers.user_id == user_id).all()
354 PullRequestReviewers.user_id == user_id).all()
355 ]
355 ]
356
356
357 def _prepare_participating_query(self, user_id=None, statuses=None,
357 def _prepare_participating_query(self, user_id=None, statuses=None,
358 order_by=None, order_dir='desc'):
358 order_by=None, order_dir='desc'):
359 q = PullRequest.query()
359 q = PullRequest.query()
360 if user_id:
360 if user_id:
361 reviewers_subquery = Session().query(
361 reviewers_subquery = Session().query(
362 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.pull_request_id).filter(
363 PullRequestReviewers.user_id == user_id).subquery()
363 PullRequestReviewers.user_id == user_id).subquery()
364 user_filter= or_(
364 user_filter= or_(
365 PullRequest.user_id == user_id,
365 PullRequest.user_id == user_id,
366 PullRequest.pull_request_id.in_(reviewers_subquery)
366 PullRequest.pull_request_id.in_(reviewers_subquery)
367 )
367 )
368 q = PullRequest.query().filter(user_filter)
368 q = PullRequest.query().filter(user_filter)
369
369
370 # closed,opened
370 # closed,opened
371 if statuses:
371 if statuses:
372 q = q.filter(PullRequest.status.in_(statuses))
372 q = q.filter(PullRequest.status.in_(statuses))
373
373
374 if order_by:
374 if order_by:
375 order_map = {
375 order_map = {
376 'name_raw': PullRequest.pull_request_id,
376 'name_raw': PullRequest.pull_request_id,
377 'title': PullRequest.title,
377 'title': PullRequest.title,
378 'updated_on_raw': PullRequest.updated_on,
378 'updated_on_raw': PullRequest.updated_on,
379 'target_repo': PullRequest.target_repo_id
379 'target_repo': PullRequest.target_repo_id
380 }
380 }
381 if order_dir == 'asc':
381 if order_dir == 'asc':
382 q = q.order_by(order_map[order_by].asc())
382 q = q.order_by(order_map[order_by].asc())
383 else:
383 else:
384 q = q.order_by(order_map[order_by].desc())
384 q = q.order_by(order_map[order_by].desc())
385
385
386 return q
386 return q
387
387
388 def count_im_participating_in(self, user_id=None, statuses=None):
388 def count_im_participating_in(self, user_id=None, statuses=None):
389 q = self._prepare_participating_query(user_id, statuses=statuses)
389 q = self._prepare_participating_query(user_id, statuses=statuses)
390 return q.count()
390 return q.count()
391
391
392 def get_im_participating_in(
392 def get_im_participating_in(
393 self, user_id=None, statuses=None, offset=0,
393 self, user_id=None, statuses=None, offset=0,
394 length=None, order_by=None, order_dir='desc'):
394 length=None, order_by=None, order_dir='desc'):
395 """
395 """
396 Get all Pull requests that i'm participating in, or i have opened
396 Get all Pull requests that i'm participating in, or i have opened
397 """
397 """
398
398
399 q = self._prepare_participating_query(
399 q = self._prepare_participating_query(
400 user_id, statuses=statuses, order_by=order_by,
400 user_id, statuses=statuses, order_by=order_by,
401 order_dir=order_dir)
401 order_dir=order_dir)
402
402
403 if length:
403 if length:
404 pull_requests = q.limit(length).offset(offset).all()
404 pull_requests = q.limit(length).offset(offset).all()
405 else:
405 else:
406 pull_requests = q.all()
406 pull_requests = q.all()
407
407
408 return pull_requests
408 return pull_requests
409
409
410 def get_versions(self, pull_request):
410 def get_versions(self, pull_request):
411 """
411 """
412 returns version of pull request sorted by ID descending
412 returns version of pull request sorted by ID descending
413 """
413 """
414 return PullRequestVersion.query()\
414 return PullRequestVersion.query()\
415 .filter(PullRequestVersion.pull_request == pull_request)\
415 .filter(PullRequestVersion.pull_request == pull_request)\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 .all()
417 .all()
418
418
419 def create(self, created_by, source_repo, source_ref, target_repo,
419 def create(self, created_by, source_repo, source_ref, target_repo,
420 target_ref, revisions, reviewers, title, description=None,
420 target_ref, revisions, reviewers, title, description=None,
421 reviewer_data=None):
421 reviewer_data=None):
422
422
423 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.author = created_by_user
435 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
437
437
438 Session().add(pull_request)
438 Session().add(pull_request)
439 Session().flush()
439 Session().flush()
440
440
441 reviewer_ids = set()
441 reviewer_ids = set()
442 # members / reviewers
442 # members / reviewers
443 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
444 user_id, reasons, mandatory = reviewer_object
444 user_id, reasons, mandatory = reviewer_object
445 user = self._get_user(user_id)
445 user = self._get_user(user_id)
446
446
447 # skip duplicates
447 # skip duplicates
448 if user.user_id in reviewer_ids:
448 if user.user_id in reviewer_ids:
449 continue
449 continue
450
450
451 reviewer_ids.add(user.user_id)
451 reviewer_ids.add(user.user_id)
452
452
453 reviewer = PullRequestReviewers()
453 reviewer = PullRequestReviewers()
454 reviewer.user = user
454 reviewer.user = user
455 reviewer.pull_request = pull_request
455 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
456 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
457 reviewer.mandatory = mandatory
458 Session().add(reviewer)
458 Session().add(reviewer)
459
459
460 # Set approval status to "Under Review" for all commits which are
460 # Set approval status to "Under Review" for all commits which are
461 # part of this pull request.
461 # part of this pull request.
462 ChangesetStatusModel().set_status(
462 ChangesetStatusModel().set_status(
463 repo=target_repo,
463 repo=target_repo,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 user=created_by_user,
465 user=created_by_user,
466 pull_request=pull_request
466 pull_request=pull_request
467 )
467 )
468
468
469 self.notify_reviewers(pull_request, reviewer_ids)
469 self.notify_reviewers(pull_request, reviewer_ids)
470 self._trigger_pull_request_hook(
470 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
471 pull_request, created_by_user, 'create')
472
472
473 creation_data = pull_request.get_api_data(with_merge_state=False)
473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 self._log_audit_action(
474 self._log_audit_action(
475 'repo.pull_request.create', {'data': creation_data},
475 'repo.pull_request.create', {'data': creation_data},
476 created_by_user, pull_request)
476 created_by_user, pull_request)
477
477
478 return pull_request
478 return pull_request
479
479
480 def _trigger_pull_request_hook(self, pull_request, user, action):
480 def _trigger_pull_request_hook(self, pull_request, user, action):
481 pull_request = self.__get_pull_request(pull_request)
481 pull_request = self.__get_pull_request(pull_request)
482 target_scm = pull_request.target_repo.scm_instance()
482 target_scm = pull_request.target_repo.scm_instance()
483 if action == 'create':
483 if action == 'create':
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
485 elif action == 'merge':
485 elif action == 'merge':
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
487 elif action == 'close':
487 elif action == 'close':
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
489 elif action == 'review_status_change':
489 elif action == 'review_status_change':
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
491 elif action == 'update':
491 elif action == 'update':
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
493 else:
493 else:
494 return
494 return
495
495
496 trigger_hook(
496 trigger_hook(
497 username=user.username,
497 username=user.username,
498 repo_name=pull_request.target_repo.repo_name,
498 repo_name=pull_request.target_repo.repo_name,
499 repo_alias=target_scm.alias,
499 repo_alias=target_scm.alias,
500 pull_request=pull_request)
500 pull_request=pull_request)
501
501
502 def _get_commit_ids(self, pull_request):
502 def _get_commit_ids(self, pull_request):
503 """
503 """
504 Return the commit ids of the merged pull request.
504 Return the commit ids of the merged pull request.
505
505
506 This method is not dealing correctly yet with the lack of autoupdates
506 This method is not dealing correctly yet with the lack of autoupdates
507 nor with the implicit target updates.
507 nor with the implicit target updates.
508 For example: if a commit in the source repo is already in the target it
508 For example: if a commit in the source repo is already in the target it
509 will be reported anyways.
509 will be reported anyways.
510 """
510 """
511 merge_rev = pull_request.merge_rev
511 merge_rev = pull_request.merge_rev
512 if merge_rev is None:
512 if merge_rev is None:
513 raise ValueError('This pull request was not merged yet')
513 raise ValueError('This pull request was not merged yet')
514
514
515 commit_ids = list(pull_request.revisions)
515 commit_ids = list(pull_request.revisions)
516 if merge_rev not in commit_ids:
516 if merge_rev not in commit_ids:
517 commit_ids.append(merge_rev)
517 commit_ids.append(merge_rev)
518
518
519 return commit_ids
519 return commit_ids
520
520
521 def merge(self, pull_request, user, extras):
521 def merge(self, pull_request, user, extras):
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
524 if merge_state.executed:
524 if merge_state.executed:
525 log.debug(
525 log.debug(
526 "Merge was successful, updating the pull request comments.")
526 "Merge was successful, updating the pull request comments.")
527 self._comment_and_close_pr(pull_request, user, merge_state)
527 self._comment_and_close_pr(pull_request, user, merge_state)
528
528
529 self._log_audit_action(
529 self._log_audit_action(
530 'repo.pull_request.merge',
530 'repo.pull_request.merge',
531 {'merge_state': merge_state.__dict__},
531 {'merge_state': merge_state.__dict__},
532 user, pull_request)
532 user, pull_request)
533
533
534 else:
534 else:
535 log.warn("Merge failed, not updating the pull request.")
535 log.warn("Merge failed, not updating the pull request.")
536 return merge_state
536 return merge_state
537
537
538 def _merge_pull_request(self, pull_request, user, extras):
538 def _merge_pull_request(self, pull_request, user, extras):
539 target_vcs = pull_request.target_repo.scm_instance()
539 target_vcs = pull_request.target_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
541 target_ref = self._refresh_reference(
541 target_ref = self._refresh_reference(
542 pull_request.target_ref_parts, target_vcs)
542 pull_request.target_ref_parts, target_vcs)
543
543
544 message = _(
544 message = _(
545 'Merge pull request #%(pr_id)s from '
545 'Merge pull request #%(pr_id)s from '
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 'pr_id': pull_request.pull_request_id,
547 'pr_id': pull_request.pull_request_id,
548 'source_repo': source_vcs.name,
548 'source_repo': source_vcs.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
550 'pr_title': pull_request.title
550 'pr_title': pull_request.title
551 }
551 }
552
552
553 workspace_id = self._workspace_id(pull_request)
553 workspace_id = self._workspace_id(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
555
555
556 callback_daemon, extras = prepare_callback_daemon(
556 callback_daemon, extras = prepare_callback_daemon(
557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
559
559
560 with callback_daemon:
560 with callback_daemon:
561 # TODO: johbo: Implement a clean way to run a config_override
561 # TODO: johbo: Implement a clean way to run a config_override
562 # for a single call.
562 # for a single call.
563 target_vcs.config.set(
563 target_vcs.config.set(
564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
565 merge_state = target_vcs.merge(
565 merge_state = target_vcs.merge(
566 target_ref, source_vcs, pull_request.source_ref_parts,
566 target_ref, source_vcs, pull_request.source_ref_parts,
567 workspace_id, user_name=user.username,
567 workspace_id, user_name=user.username,
568 user_email=user.email, message=message, use_rebase=use_rebase)
568 user_email=user.email, message=message, use_rebase=use_rebase)
569 return merge_state
569 return merge_state
570
570
571 def _comment_and_close_pr(self, pull_request, user, merge_state):
571 def _comment_and_close_pr(self, pull_request, user, merge_state):
572 pull_request.merge_rev = merge_state.merge_ref.commit_id
572 pull_request.merge_rev = merge_state.merge_ref.commit_id
573 pull_request.updated_on = datetime.datetime.now()
573 pull_request.updated_on = datetime.datetime.now()
574
574
575 CommentsModel().create(
575 CommentsModel().create(
576 text=unicode(_('Pull request merged and closed')),
576 text=unicode(_('Pull request merged and closed')),
577 repo=pull_request.target_repo.repo_id,
577 repo=pull_request.target_repo.repo_id,
578 user=user.user_id,
578 user=user.user_id,
579 pull_request=pull_request.pull_request_id,
579 pull_request=pull_request.pull_request_id,
580 f_path=None,
580 f_path=None,
581 line_no=None,
581 line_no=None,
582 closing_pr=True
582 closing_pr=True
583 )
583 )
584
584
585 Session().add(pull_request)
585 Session().add(pull_request)
586 Session().flush()
586 Session().flush()
587 # TODO: paris: replace invalidation with less radical solution
587 # TODO: paris: replace invalidation with less radical solution
588 ScmModel().mark_for_invalidation(
588 ScmModel().mark_for_invalidation(
589 pull_request.target_repo.repo_name)
589 pull_request.target_repo.repo_name)
590 self._trigger_pull_request_hook(pull_request, user, 'merge')
590 self._trigger_pull_request_hook(pull_request, user, 'merge')
591
591
592 def has_valid_update_type(self, pull_request):
592 def has_valid_update_type(self, pull_request):
593 source_ref_type = pull_request.source_ref_parts.type
593 source_ref_type = pull_request.source_ref_parts.type
594 return source_ref_type in ['book', 'branch', 'tag']
594 return source_ref_type in ['book', 'branch', 'tag']
595
595
596 def update_commits(self, pull_request):
596 def update_commits(self, pull_request):
597 """
597 """
598 Get the updated list of commits for the pull request
598 Get the updated list of commits for the pull request
599 and return the new pull request version and the list
599 and return the new pull request version and the list
600 of commits processed by this update action
600 of commits processed by this update action
601 """
601 """
602 pull_request = self.__get_pull_request(pull_request)
602 pull_request = self.__get_pull_request(pull_request)
603 source_ref_type = pull_request.source_ref_parts.type
603 source_ref_type = pull_request.source_ref_parts.type
604 source_ref_name = pull_request.source_ref_parts.name
604 source_ref_name = pull_request.source_ref_parts.name
605 source_ref_id = pull_request.source_ref_parts.commit_id
605 source_ref_id = pull_request.source_ref_parts.commit_id
606
606
607 target_ref_type = pull_request.target_ref_parts.type
607 target_ref_type = pull_request.target_ref_parts.type
608 target_ref_name = pull_request.target_ref_parts.name
608 target_ref_name = pull_request.target_ref_parts.name
609 target_ref_id = pull_request.target_ref_parts.commit_id
609 target_ref_id = pull_request.target_ref_parts.commit_id
610
610
611 if not self.has_valid_update_type(pull_request):
611 if not self.has_valid_update_type(pull_request):
612 log.debug(
612 log.debug(
613 "Skipping update of pull request %s due to ref type: %s",
613 "Skipping update of pull request %s due to ref type: %s",
614 pull_request, source_ref_type)
614 pull_request, source_ref_type)
615 return UpdateResponse(
615 return UpdateResponse(
616 executed=False,
616 executed=False,
617 reason=UpdateFailureReason.WRONG_REF_TYPE,
617 reason=UpdateFailureReason.WRONG_REF_TYPE,
618 old=pull_request, new=None, changes=None,
618 old=pull_request, new=None, changes=None,
619 source_changed=False, target_changed=False)
619 source_changed=False, target_changed=False)
620
620
621 # source repo
621 # source repo
622 source_repo = pull_request.source_repo.scm_instance()
622 source_repo = pull_request.source_repo.scm_instance()
623 try:
623 try:
624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
625 except CommitDoesNotExistError:
625 except CommitDoesNotExistError:
626 return UpdateResponse(
626 return UpdateResponse(
627 executed=False,
627 executed=False,
628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
629 old=pull_request, new=None, changes=None,
629 old=pull_request, new=None, changes=None,
630 source_changed=False, target_changed=False)
630 source_changed=False, target_changed=False)
631
631
632 source_changed = source_ref_id != source_commit.raw_id
632 source_changed = source_ref_id != source_commit.raw_id
633
633
634 # target repo
634 # target repo
635 target_repo = pull_request.target_repo.scm_instance()
635 target_repo = pull_request.target_repo.scm_instance()
636 try:
636 try:
637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
638 except CommitDoesNotExistError:
638 except CommitDoesNotExistError:
639 return UpdateResponse(
639 return UpdateResponse(
640 executed=False,
640 executed=False,
641 reason=UpdateFailureReason.MISSING_TARGET_REF,
641 reason=UpdateFailureReason.MISSING_TARGET_REF,
642 old=pull_request, new=None, changes=None,
642 old=pull_request, new=None, changes=None,
643 source_changed=False, target_changed=False)
643 source_changed=False, target_changed=False)
644 target_changed = target_ref_id != target_commit.raw_id
644 target_changed = target_ref_id != target_commit.raw_id
645
645
646 if not (source_changed or target_changed):
646 if not (source_changed or target_changed):
647 log.debug("Nothing changed in pull request %s", pull_request)
647 log.debug("Nothing changed in pull request %s", pull_request)
648 return UpdateResponse(
648 return UpdateResponse(
649 executed=False,
649 executed=False,
650 reason=UpdateFailureReason.NO_CHANGE,
650 reason=UpdateFailureReason.NO_CHANGE,
651 old=pull_request, new=None, changes=None,
651 old=pull_request, new=None, changes=None,
652 source_changed=target_changed, target_changed=source_changed)
652 source_changed=target_changed, target_changed=source_changed)
653
653
654 change_in_found = 'target repo' if target_changed else 'source repo'
654 change_in_found = 'target repo' if target_changed else 'source repo'
655 log.debug('Updating pull request because of change in %s detected',
655 log.debug('Updating pull request because of change in %s detected',
656 change_in_found)
656 change_in_found)
657
657
658 # Finally there is a need for an update, in case of source change
658 # Finally there is a need for an update, in case of source change
659 # we create a new version, else just an update
659 # we create a new version, else just an update
660 if source_changed:
660 if source_changed:
661 pull_request_version = self._create_version_from_snapshot(pull_request)
661 pull_request_version = self._create_version_from_snapshot(pull_request)
662 self._link_comments_to_version(pull_request_version)
662 self._link_comments_to_version(pull_request_version)
663 else:
663 else:
664 try:
664 try:
665 ver = pull_request.versions[-1]
665 ver = pull_request.versions[-1]
666 except IndexError:
666 except IndexError:
667 ver = None
667 ver = None
668
668
669 pull_request.pull_request_version_id = \
669 pull_request.pull_request_version_id = \
670 ver.pull_request_version_id if ver else None
670 ver.pull_request_version_id if ver else None
671 pull_request_version = pull_request
671 pull_request_version = pull_request
672
672
673 try:
673 try:
674 if target_ref_type in ('tag', 'branch', 'book'):
674 if target_ref_type in ('tag', 'branch', 'book'):
675 target_commit = target_repo.get_commit(target_ref_name)
675 target_commit = target_repo.get_commit(target_ref_name)
676 else:
676 else:
677 target_commit = target_repo.get_commit(target_ref_id)
677 target_commit = target_repo.get_commit(target_ref_id)
678 except CommitDoesNotExistError:
678 except CommitDoesNotExistError:
679 return UpdateResponse(
679 return UpdateResponse(
680 executed=False,
680 executed=False,
681 reason=UpdateFailureReason.MISSING_TARGET_REF,
681 reason=UpdateFailureReason.MISSING_TARGET_REF,
682 old=pull_request, new=None, changes=None,
682 old=pull_request, new=None, changes=None,
683 source_changed=source_changed, target_changed=target_changed)
683 source_changed=source_changed, target_changed=target_changed)
684
684
685 # re-compute commit ids
685 # re-compute commit ids
686 old_commit_ids = pull_request.revisions
686 old_commit_ids = pull_request.revisions
687 pre_load = ["author", "branch", "date", "message"]
687 pre_load = ["author", "branch", "date", "message"]
688 commit_ranges = target_repo.compare(
688 commit_ranges = target_repo.compare(
689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
690 pre_load=pre_load)
690 pre_load=pre_load)
691
691
692 ancestor = target_repo.get_common_ancestor(
692 ancestor = target_repo.get_common_ancestor(
693 target_commit.raw_id, source_commit.raw_id, source_repo)
693 target_commit.raw_id, source_commit.raw_id, source_repo)
694
694
695 pull_request.source_ref = '%s:%s:%s' % (
695 pull_request.source_ref = '%s:%s:%s' % (
696 source_ref_type, source_ref_name, source_commit.raw_id)
696 source_ref_type, source_ref_name, source_commit.raw_id)
697 pull_request.target_ref = '%s:%s:%s' % (
697 pull_request.target_ref = '%s:%s:%s' % (
698 target_ref_type, target_ref_name, ancestor)
698 target_ref_type, target_ref_name, ancestor)
699
699
700 pull_request.revisions = [
700 pull_request.revisions = [
701 commit.raw_id for commit in reversed(commit_ranges)]
701 commit.raw_id for commit in reversed(commit_ranges)]
702 pull_request.updated_on = datetime.datetime.now()
702 pull_request.updated_on = datetime.datetime.now()
703 Session().add(pull_request)
703 Session().add(pull_request)
704 new_commit_ids = pull_request.revisions
704 new_commit_ids = pull_request.revisions
705
705
706 old_diff_data, new_diff_data = self._generate_update_diffs(
706 old_diff_data, new_diff_data = self._generate_update_diffs(
707 pull_request, pull_request_version)
707 pull_request, pull_request_version)
708
708
709 # calculate commit and file changes
709 # calculate commit and file changes
710 changes = self._calculate_commit_id_changes(
710 changes = self._calculate_commit_id_changes(
711 old_commit_ids, new_commit_ids)
711 old_commit_ids, new_commit_ids)
712 file_changes = self._calculate_file_changes(
712 file_changes = self._calculate_file_changes(
713 old_diff_data, new_diff_data)
713 old_diff_data, new_diff_data)
714
714
715 # set comments as outdated if DIFFS changed
715 # set comments as outdated if DIFFS changed
716 CommentsModel().outdate_comments(
716 CommentsModel().outdate_comments(
717 pull_request, old_diff_data=old_diff_data,
717 pull_request, old_diff_data=old_diff_data,
718 new_diff_data=new_diff_data)
718 new_diff_data=new_diff_data)
719
719
720 commit_changes = (changes.added or changes.removed)
720 commit_changes = (changes.added or changes.removed)
721 file_node_changes = (
721 file_node_changes = (
722 file_changes.added or file_changes.modified or file_changes.removed)
722 file_changes.added or file_changes.modified or file_changes.removed)
723 pr_has_changes = commit_changes or file_node_changes
723 pr_has_changes = commit_changes or file_node_changes
724
724
725 # Add an automatic comment to the pull request, in case
725 # Add an automatic comment to the pull request, in case
726 # anything has changed
726 # anything has changed
727 if pr_has_changes:
727 if pr_has_changes:
728 update_comment = CommentsModel().create(
728 update_comment = CommentsModel().create(
729 text=self._render_update_message(changes, file_changes),
729 text=self._render_update_message(changes, file_changes),
730 repo=pull_request.target_repo,
730 repo=pull_request.target_repo,
731 user=pull_request.author,
731 user=pull_request.author,
732 pull_request=pull_request,
732 pull_request=pull_request,
733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
734
734
735 # Update status to "Under Review" for added commits
735 # Update status to "Under Review" for added commits
736 for commit_id in changes.added:
736 for commit_id in changes.added:
737 ChangesetStatusModel().set_status(
737 ChangesetStatusModel().set_status(
738 repo=pull_request.source_repo,
738 repo=pull_request.source_repo,
739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
740 comment=update_comment,
740 comment=update_comment,
741 user=pull_request.author,
741 user=pull_request.author,
742 pull_request=pull_request,
742 pull_request=pull_request,
743 revision=commit_id)
743 revision=commit_id)
744
744
745 log.debug(
745 log.debug(
746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
747 'removed_ids: %s', pull_request.pull_request_id,
747 'removed_ids: %s', pull_request.pull_request_id,
748 changes.added, changes.common, changes.removed)
748 changes.added, changes.common, changes.removed)
749 log.debug(
749 log.debug(
750 'Updated pull request with the following file changes: %s',
750 'Updated pull request with the following file changes: %s',
751 file_changes)
751 file_changes)
752
752
753 log.info(
753 log.info(
754 "Updated pull request %s from commit %s to commit %s, "
754 "Updated pull request %s from commit %s to commit %s, "
755 "stored new version %s of this pull request.",
755 "stored new version %s of this pull request.",
756 pull_request.pull_request_id, source_ref_id,
756 pull_request.pull_request_id, source_ref_id,
757 pull_request.source_ref_parts.commit_id,
757 pull_request.source_ref_parts.commit_id,
758 pull_request_version.pull_request_version_id)
758 pull_request_version.pull_request_version_id)
759 Session().commit()
759 Session().commit()
760 self._trigger_pull_request_hook(
760 self._trigger_pull_request_hook(
761 pull_request, pull_request.author, 'update')
761 pull_request, pull_request.author, 'update')
762
762
763 return UpdateResponse(
763 return UpdateResponse(
764 executed=True, reason=UpdateFailureReason.NONE,
764 executed=True, reason=UpdateFailureReason.NONE,
765 old=pull_request, new=pull_request_version, changes=changes,
765 old=pull_request, new=pull_request_version, changes=changes,
766 source_changed=source_changed, target_changed=target_changed)
766 source_changed=source_changed, target_changed=target_changed)
767
767
768 def _create_version_from_snapshot(self, pull_request):
768 def _create_version_from_snapshot(self, pull_request):
769 version = PullRequestVersion()
769 version = PullRequestVersion()
770 version.title = pull_request.title
770 version.title = pull_request.title
771 version.description = pull_request.description
771 version.description = pull_request.description
772 version.status = pull_request.status
772 version.status = pull_request.status
773 version.created_on = datetime.datetime.now()
773 version.created_on = datetime.datetime.now()
774 version.updated_on = pull_request.updated_on
774 version.updated_on = pull_request.updated_on
775 version.user_id = pull_request.user_id
775 version.user_id = pull_request.user_id
776 version.source_repo = pull_request.source_repo
776 version.source_repo = pull_request.source_repo
777 version.source_ref = pull_request.source_ref
777 version.source_ref = pull_request.source_ref
778 version.target_repo = pull_request.target_repo
778 version.target_repo = pull_request.target_repo
779 version.target_ref = pull_request.target_ref
779 version.target_ref = pull_request.target_ref
780
780
781 version._last_merge_source_rev = pull_request._last_merge_source_rev
781 version._last_merge_source_rev = pull_request._last_merge_source_rev
782 version._last_merge_target_rev = pull_request._last_merge_target_rev
782 version._last_merge_target_rev = pull_request._last_merge_target_rev
783 version._last_merge_status = pull_request._last_merge_status
783 version.last_merge_status = pull_request.last_merge_status
784 version.shadow_merge_ref = pull_request.shadow_merge_ref
784 version.shadow_merge_ref = pull_request.shadow_merge_ref
785 version.merge_rev = pull_request.merge_rev
785 version.merge_rev = pull_request.merge_rev
786 version.reviewer_data = pull_request.reviewer_data
786 version.reviewer_data = pull_request.reviewer_data
787
787
788 version.revisions = pull_request.revisions
788 version.revisions = pull_request.revisions
789 version.pull_request = pull_request
789 version.pull_request = pull_request
790 Session().add(version)
790 Session().add(version)
791 Session().flush()
791 Session().flush()
792
792
793 return version
793 return version
794
794
795 def _generate_update_diffs(self, pull_request, pull_request_version):
795 def _generate_update_diffs(self, pull_request, pull_request_version):
796
796
797 diff_context = (
797 diff_context = (
798 self.DIFF_CONTEXT +
798 self.DIFF_CONTEXT +
799 CommentsModel.needed_extra_diff_context())
799 CommentsModel.needed_extra_diff_context())
800
800
801 source_repo = pull_request_version.source_repo
801 source_repo = pull_request_version.source_repo
802 source_ref_id = pull_request_version.source_ref_parts.commit_id
802 source_ref_id = pull_request_version.source_ref_parts.commit_id
803 target_ref_id = pull_request_version.target_ref_parts.commit_id
803 target_ref_id = pull_request_version.target_ref_parts.commit_id
804 old_diff = self._get_diff_from_pr_or_version(
804 old_diff = self._get_diff_from_pr_or_version(
805 source_repo, source_ref_id, target_ref_id, context=diff_context)
805 source_repo, source_ref_id, target_ref_id, context=diff_context)
806
806
807 source_repo = pull_request.source_repo
807 source_repo = pull_request.source_repo
808 source_ref_id = pull_request.source_ref_parts.commit_id
808 source_ref_id = pull_request.source_ref_parts.commit_id
809 target_ref_id = pull_request.target_ref_parts.commit_id
809 target_ref_id = pull_request.target_ref_parts.commit_id
810
810
811 new_diff = self._get_diff_from_pr_or_version(
811 new_diff = self._get_diff_from_pr_or_version(
812 source_repo, source_ref_id, target_ref_id, context=diff_context)
812 source_repo, source_ref_id, target_ref_id, context=diff_context)
813
813
814 old_diff_data = diffs.DiffProcessor(old_diff)
814 old_diff_data = diffs.DiffProcessor(old_diff)
815 old_diff_data.prepare()
815 old_diff_data.prepare()
816 new_diff_data = diffs.DiffProcessor(new_diff)
816 new_diff_data = diffs.DiffProcessor(new_diff)
817 new_diff_data.prepare()
817 new_diff_data.prepare()
818
818
819 return old_diff_data, new_diff_data
819 return old_diff_data, new_diff_data
820
820
821 def _link_comments_to_version(self, pull_request_version):
821 def _link_comments_to_version(self, pull_request_version):
822 """
822 """
823 Link all unlinked comments of this pull request to the given version.
823 Link all unlinked comments of this pull request to the given version.
824
824
825 :param pull_request_version: The `PullRequestVersion` to which
825 :param pull_request_version: The `PullRequestVersion` to which
826 the comments shall be linked.
826 the comments shall be linked.
827
827
828 """
828 """
829 pull_request = pull_request_version.pull_request
829 pull_request = pull_request_version.pull_request
830 comments = ChangesetComment.query()\
830 comments = ChangesetComment.query()\
831 .filter(
831 .filter(
832 # TODO: johbo: Should we query for the repo at all here?
832 # TODO: johbo: Should we query for the repo at all here?
833 # Pending decision on how comments of PRs are to be related
833 # Pending decision on how comments of PRs are to be related
834 # to either the source repo, the target repo or no repo at all.
834 # to either the source repo, the target repo or no repo at all.
835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
836 ChangesetComment.pull_request == pull_request,
836 ChangesetComment.pull_request == pull_request,
837 ChangesetComment.pull_request_version == None)\
837 ChangesetComment.pull_request_version == None)\
838 .order_by(ChangesetComment.comment_id.asc())
838 .order_by(ChangesetComment.comment_id.asc())
839
839
840 # TODO: johbo: Find out why this breaks if it is done in a bulk
840 # TODO: johbo: Find out why this breaks if it is done in a bulk
841 # operation.
841 # operation.
842 for comment in comments:
842 for comment in comments:
843 comment.pull_request_version_id = (
843 comment.pull_request_version_id = (
844 pull_request_version.pull_request_version_id)
844 pull_request_version.pull_request_version_id)
845 Session().add(comment)
845 Session().add(comment)
846
846
847 def _calculate_commit_id_changes(self, old_ids, new_ids):
847 def _calculate_commit_id_changes(self, old_ids, new_ids):
848 added = [x for x in new_ids if x not in old_ids]
848 added = [x for x in new_ids if x not in old_ids]
849 common = [x for x in new_ids if x in old_ids]
849 common = [x for x in new_ids if x in old_ids]
850 removed = [x for x in old_ids if x not in new_ids]
850 removed = [x for x in old_ids if x not in new_ids]
851 total = new_ids
851 total = new_ids
852 return ChangeTuple(added, common, removed, total)
852 return ChangeTuple(added, common, removed, total)
853
853
854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
855
855
856 old_files = OrderedDict()
856 old_files = OrderedDict()
857 for diff_data in old_diff_data.parsed_diff:
857 for diff_data in old_diff_data.parsed_diff:
858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
859
859
860 added_files = []
860 added_files = []
861 modified_files = []
861 modified_files = []
862 removed_files = []
862 removed_files = []
863 for diff_data in new_diff_data.parsed_diff:
863 for diff_data in new_diff_data.parsed_diff:
864 new_filename = diff_data['filename']
864 new_filename = diff_data['filename']
865 new_hash = md5_safe(diff_data['raw_diff'])
865 new_hash = md5_safe(diff_data['raw_diff'])
866
866
867 old_hash = old_files.get(new_filename)
867 old_hash = old_files.get(new_filename)
868 if not old_hash:
868 if not old_hash:
869 # file is not present in old diff, means it's added
869 # file is not present in old diff, means it's added
870 added_files.append(new_filename)
870 added_files.append(new_filename)
871 else:
871 else:
872 if new_hash != old_hash:
872 if new_hash != old_hash:
873 modified_files.append(new_filename)
873 modified_files.append(new_filename)
874 # now remove a file from old, since we have seen it already
874 # now remove a file from old, since we have seen it already
875 del old_files[new_filename]
875 del old_files[new_filename]
876
876
877 # removed files is when there are present in old, but not in NEW,
877 # removed files is when there are present in old, but not in NEW,
878 # since we remove old files that are present in new diff, left-overs
878 # since we remove old files that are present in new diff, left-overs
879 # if any should be the removed files
879 # if any should be the removed files
880 removed_files.extend(old_files.keys())
880 removed_files.extend(old_files.keys())
881
881
882 return FileChangeTuple(added_files, modified_files, removed_files)
882 return FileChangeTuple(added_files, modified_files, removed_files)
883
883
884 def _render_update_message(self, changes, file_changes):
884 def _render_update_message(self, changes, file_changes):
885 """
885 """
886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
887 so it's always looking the same disregarding on which default
887 so it's always looking the same disregarding on which default
888 renderer system is using.
888 renderer system is using.
889
889
890 :param changes: changes named tuple
890 :param changes: changes named tuple
891 :param file_changes: file changes named tuple
891 :param file_changes: file changes named tuple
892
892
893 """
893 """
894 new_status = ChangesetStatus.get_status_lbl(
894 new_status = ChangesetStatus.get_status_lbl(
895 ChangesetStatus.STATUS_UNDER_REVIEW)
895 ChangesetStatus.STATUS_UNDER_REVIEW)
896
896
897 changed_files = (
897 changed_files = (
898 file_changes.added + file_changes.modified + file_changes.removed)
898 file_changes.added + file_changes.modified + file_changes.removed)
899
899
900 params = {
900 params = {
901 'under_review_label': new_status,
901 'under_review_label': new_status,
902 'added_commits': changes.added,
902 'added_commits': changes.added,
903 'removed_commits': changes.removed,
903 'removed_commits': changes.removed,
904 'changed_files': changed_files,
904 'changed_files': changed_files,
905 'added_files': file_changes.added,
905 'added_files': file_changes.added,
906 'modified_files': file_changes.modified,
906 'modified_files': file_changes.modified,
907 'removed_files': file_changes.removed,
907 'removed_files': file_changes.removed,
908 }
908 }
909 renderer = RstTemplateRenderer()
909 renderer = RstTemplateRenderer()
910 return renderer.render('pull_request_update.mako', **params)
910 return renderer.render('pull_request_update.mako', **params)
911
911
912 def edit(self, pull_request, title, description, user):
912 def edit(self, pull_request, title, description, user):
913 pull_request = self.__get_pull_request(pull_request)
913 pull_request = self.__get_pull_request(pull_request)
914 old_data = pull_request.get_api_data(with_merge_state=False)
914 old_data = pull_request.get_api_data(with_merge_state=False)
915 if pull_request.is_closed():
915 if pull_request.is_closed():
916 raise ValueError('This pull request is closed')
916 raise ValueError('This pull request is closed')
917 if title:
917 if title:
918 pull_request.title = title
918 pull_request.title = title
919 pull_request.description = description
919 pull_request.description = description
920 pull_request.updated_on = datetime.datetime.now()
920 pull_request.updated_on = datetime.datetime.now()
921 Session().add(pull_request)
921 Session().add(pull_request)
922 self._log_audit_action(
922 self._log_audit_action(
923 'repo.pull_request.edit', {'old_data': old_data},
923 'repo.pull_request.edit', {'old_data': old_data},
924 user, pull_request)
924 user, pull_request)
925
925
926 def update_reviewers(self, pull_request, reviewer_data, user):
926 def update_reviewers(self, pull_request, reviewer_data, user):
927 """
927 """
928 Update the reviewers in the pull request
928 Update the reviewers in the pull request
929
929
930 :param pull_request: the pr to update
930 :param pull_request: the pr to update
931 :param reviewer_data: list of tuples
931 :param reviewer_data: list of tuples
932 [(user, ['reason1', 'reason2'], mandatory_flag)]
932 [(user, ['reason1', 'reason2'], mandatory_flag)]
933 """
933 """
934
934
935 reviewers = {}
935 reviewers = {}
936 for user_id, reasons, mandatory in reviewer_data:
936 for user_id, reasons, mandatory in reviewer_data:
937 if isinstance(user_id, (int, basestring)):
937 if isinstance(user_id, (int, basestring)):
938 user_id = self._get_user(user_id).user_id
938 user_id = self._get_user(user_id).user_id
939 reviewers[user_id] = {
939 reviewers[user_id] = {
940 'reasons': reasons, 'mandatory': mandatory}
940 'reasons': reasons, 'mandatory': mandatory}
941
941
942 reviewers_ids = set(reviewers.keys())
942 reviewers_ids = set(reviewers.keys())
943 pull_request = self.__get_pull_request(pull_request)
943 pull_request = self.__get_pull_request(pull_request)
944 current_reviewers = PullRequestReviewers.query()\
944 current_reviewers = PullRequestReviewers.query()\
945 .filter(PullRequestReviewers.pull_request ==
945 .filter(PullRequestReviewers.pull_request ==
946 pull_request).all()
946 pull_request).all()
947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
948
948
949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
951
951
952 log.debug("Adding %s reviewers", ids_to_add)
952 log.debug("Adding %s reviewers", ids_to_add)
953 log.debug("Removing %s reviewers", ids_to_remove)
953 log.debug("Removing %s reviewers", ids_to_remove)
954 changed = False
954 changed = False
955 for uid in ids_to_add:
955 for uid in ids_to_add:
956 changed = True
956 changed = True
957 _usr = self._get_user(uid)
957 _usr = self._get_user(uid)
958 reviewer = PullRequestReviewers()
958 reviewer = PullRequestReviewers()
959 reviewer.user = _usr
959 reviewer.user = _usr
960 reviewer.pull_request = pull_request
960 reviewer.pull_request = pull_request
961 reviewer.reasons = reviewers[uid]['reasons']
961 reviewer.reasons = reviewers[uid]['reasons']
962 # NOTE(marcink): mandatory shouldn't be changed now
962 # NOTE(marcink): mandatory shouldn't be changed now
963 # reviewer.mandatory = reviewers[uid]['reasons']
963 # reviewer.mandatory = reviewers[uid]['reasons']
964 Session().add(reviewer)
964 Session().add(reviewer)
965 self._log_audit_action(
965 self._log_audit_action(
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
967 user, pull_request)
967 user, pull_request)
968
968
969 for uid in ids_to_remove:
969 for uid in ids_to_remove:
970 changed = True
970 changed = True
971 reviewers = PullRequestReviewers.query()\
971 reviewers = PullRequestReviewers.query()\
972 .filter(PullRequestReviewers.user_id == uid,
972 .filter(PullRequestReviewers.user_id == uid,
973 PullRequestReviewers.pull_request == pull_request)\
973 PullRequestReviewers.pull_request == pull_request)\
974 .all()
974 .all()
975 # use .all() in case we accidentally added the same person twice
975 # use .all() in case we accidentally added the same person twice
976 # this CAN happen due to the lack of DB checks
976 # this CAN happen due to the lack of DB checks
977 for obj in reviewers:
977 for obj in reviewers:
978 old_data = obj.get_dict()
978 old_data = obj.get_dict()
979 Session().delete(obj)
979 Session().delete(obj)
980 self._log_audit_action(
980 self._log_audit_action(
981 'repo.pull_request.reviewer.delete',
981 'repo.pull_request.reviewer.delete',
982 {'old_data': old_data}, user, pull_request)
982 {'old_data': old_data}, user, pull_request)
983
983
984 if changed:
984 if changed:
985 pull_request.updated_on = datetime.datetime.now()
985 pull_request.updated_on = datetime.datetime.now()
986 Session().add(pull_request)
986 Session().add(pull_request)
987
987
988 self.notify_reviewers(pull_request, ids_to_add)
988 self.notify_reviewers(pull_request, ids_to_add)
989 return ids_to_add, ids_to_remove
989 return ids_to_add, ids_to_remove
990
990
991 def get_url(self, pull_request, request=None, permalink=False):
991 def get_url(self, pull_request, request=None, permalink=False):
992 if not request:
992 if not request:
993 request = get_current_request()
993 request = get_current_request()
994
994
995 if permalink:
995 if permalink:
996 return request.route_url(
996 return request.route_url(
997 'pull_requests_global',
997 'pull_requests_global',
998 pull_request_id=pull_request.pull_request_id,)
998 pull_request_id=pull_request.pull_request_id,)
999 else:
999 else:
1000 return request.route_url('pullrequest_show',
1000 return request.route_url('pullrequest_show',
1001 repo_name=safe_str(pull_request.target_repo.repo_name),
1001 repo_name=safe_str(pull_request.target_repo.repo_name),
1002 pull_request_id=pull_request.pull_request_id,)
1002 pull_request_id=pull_request.pull_request_id,)
1003
1003
1004 def get_shadow_clone_url(self, pull_request):
1004 def get_shadow_clone_url(self, pull_request):
1005 """
1005 """
1006 Returns qualified url pointing to the shadow repository. If this pull
1006 Returns qualified url pointing to the shadow repository. If this pull
1007 request is closed there is no shadow repository and ``None`` will be
1007 request is closed there is no shadow repository and ``None`` will be
1008 returned.
1008 returned.
1009 """
1009 """
1010 if pull_request.is_closed():
1010 if pull_request.is_closed():
1011 return None
1011 return None
1012 else:
1012 else:
1013 pr_url = urllib.unquote(self.get_url(pull_request))
1013 pr_url = urllib.unquote(self.get_url(pull_request))
1014 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1014 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1015
1015
1016 def notify_reviewers(self, pull_request, reviewers_ids):
1016 def notify_reviewers(self, pull_request, reviewers_ids):
1017 # notification to reviewers
1017 # notification to reviewers
1018 if not reviewers_ids:
1018 if not reviewers_ids:
1019 return
1019 return
1020
1020
1021 pull_request_obj = pull_request
1021 pull_request_obj = pull_request
1022 # get the current participants of this pull request
1022 # get the current participants of this pull request
1023 recipients = reviewers_ids
1023 recipients = reviewers_ids
1024 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1024 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1025
1025
1026 pr_source_repo = pull_request_obj.source_repo
1026 pr_source_repo = pull_request_obj.source_repo
1027 pr_target_repo = pull_request_obj.target_repo
1027 pr_target_repo = pull_request_obj.target_repo
1028
1028
1029 pr_url = h.route_url('pullrequest_show',
1029 pr_url = h.route_url('pullrequest_show',
1030 repo_name=pr_target_repo.repo_name,
1030 repo_name=pr_target_repo.repo_name,
1031 pull_request_id=pull_request_obj.pull_request_id,)
1031 pull_request_id=pull_request_obj.pull_request_id,)
1032
1032
1033 # set some variables for email notification
1033 # set some variables for email notification
1034 pr_target_repo_url = h.route_url(
1034 pr_target_repo_url = h.route_url(
1035 'repo_summary', repo_name=pr_target_repo.repo_name)
1035 'repo_summary', repo_name=pr_target_repo.repo_name)
1036
1036
1037 pr_source_repo_url = h.route_url(
1037 pr_source_repo_url = h.route_url(
1038 'repo_summary', repo_name=pr_source_repo.repo_name)
1038 'repo_summary', repo_name=pr_source_repo.repo_name)
1039
1039
1040 # pull request specifics
1040 # pull request specifics
1041 pull_request_commits = [
1041 pull_request_commits = [
1042 (x.raw_id, x.message)
1042 (x.raw_id, x.message)
1043 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1043 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1044
1044
1045 kwargs = {
1045 kwargs = {
1046 'user': pull_request.author,
1046 'user': pull_request.author,
1047 'pull_request': pull_request_obj,
1047 'pull_request': pull_request_obj,
1048 'pull_request_commits': pull_request_commits,
1048 'pull_request_commits': pull_request_commits,
1049
1049
1050 'pull_request_target_repo': pr_target_repo,
1050 'pull_request_target_repo': pr_target_repo,
1051 'pull_request_target_repo_url': pr_target_repo_url,
1051 'pull_request_target_repo_url': pr_target_repo_url,
1052
1052
1053 'pull_request_source_repo': pr_source_repo,
1053 'pull_request_source_repo': pr_source_repo,
1054 'pull_request_source_repo_url': pr_source_repo_url,
1054 'pull_request_source_repo_url': pr_source_repo_url,
1055
1055
1056 'pull_request_url': pr_url,
1056 'pull_request_url': pr_url,
1057 }
1057 }
1058
1058
1059 # pre-generate the subject for notification itself
1059 # pre-generate the subject for notification itself
1060 (subject,
1060 (subject,
1061 _h, _e, # we don't care about those
1061 _h, _e, # we don't care about those
1062 body_plaintext) = EmailNotificationModel().render_email(
1062 body_plaintext) = EmailNotificationModel().render_email(
1063 notification_type, **kwargs)
1063 notification_type, **kwargs)
1064
1064
1065 # create notification objects, and emails
1065 # create notification objects, and emails
1066 NotificationModel().create(
1066 NotificationModel().create(
1067 created_by=pull_request.author,
1067 created_by=pull_request.author,
1068 notification_subject=subject,
1068 notification_subject=subject,
1069 notification_body=body_plaintext,
1069 notification_body=body_plaintext,
1070 notification_type=notification_type,
1070 notification_type=notification_type,
1071 recipients=recipients,
1071 recipients=recipients,
1072 email_kwargs=kwargs,
1072 email_kwargs=kwargs,
1073 )
1073 )
1074
1074
1075 def delete(self, pull_request, user):
1075 def delete(self, pull_request, user):
1076 pull_request = self.__get_pull_request(pull_request)
1076 pull_request = self.__get_pull_request(pull_request)
1077 old_data = pull_request.get_api_data(with_merge_state=False)
1077 old_data = pull_request.get_api_data(with_merge_state=False)
1078 self._cleanup_merge_workspace(pull_request)
1078 self._cleanup_merge_workspace(pull_request)
1079 self._log_audit_action(
1079 self._log_audit_action(
1080 'repo.pull_request.delete', {'old_data': old_data},
1080 'repo.pull_request.delete', {'old_data': old_data},
1081 user, pull_request)
1081 user, pull_request)
1082 Session().delete(pull_request)
1082 Session().delete(pull_request)
1083
1083
1084 def close_pull_request(self, pull_request, user):
1084 def close_pull_request(self, pull_request, user):
1085 pull_request = self.__get_pull_request(pull_request)
1085 pull_request = self.__get_pull_request(pull_request)
1086 self._cleanup_merge_workspace(pull_request)
1086 self._cleanup_merge_workspace(pull_request)
1087 pull_request.status = PullRequest.STATUS_CLOSED
1087 pull_request.status = PullRequest.STATUS_CLOSED
1088 pull_request.updated_on = datetime.datetime.now()
1088 pull_request.updated_on = datetime.datetime.now()
1089 Session().add(pull_request)
1089 Session().add(pull_request)
1090 self._trigger_pull_request_hook(
1090 self._trigger_pull_request_hook(
1091 pull_request, pull_request.author, 'close')
1091 pull_request, pull_request.author, 'close')
1092 self._log_audit_action(
1092 self._log_audit_action(
1093 'repo.pull_request.close', {}, user, pull_request)
1093 'repo.pull_request.close', {}, user, pull_request)
1094
1094
1095 def close_pull_request_with_comment(
1095 def close_pull_request_with_comment(
1096 self, pull_request, user, repo, message=None):
1096 self, pull_request, user, repo, message=None):
1097
1097
1098 pull_request_review_status = pull_request.calculated_review_status()
1098 pull_request_review_status = pull_request.calculated_review_status()
1099
1099
1100 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1100 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1101 # approved only if we have voting consent
1101 # approved only if we have voting consent
1102 status = ChangesetStatus.STATUS_APPROVED
1102 status = ChangesetStatus.STATUS_APPROVED
1103 else:
1103 else:
1104 status = ChangesetStatus.STATUS_REJECTED
1104 status = ChangesetStatus.STATUS_REJECTED
1105 status_lbl = ChangesetStatus.get_status_lbl(status)
1105 status_lbl = ChangesetStatus.get_status_lbl(status)
1106
1106
1107 default_message = (
1107 default_message = (
1108 _('Closing with status change {transition_icon} {status}.')
1108 _('Closing with status change {transition_icon} {status}.')
1109 ).format(transition_icon='>', status=status_lbl)
1109 ).format(transition_icon='>', status=status_lbl)
1110 text = message or default_message
1110 text = message or default_message
1111
1111
1112 # create a comment, and link it to new status
1112 # create a comment, and link it to new status
1113 comment = CommentsModel().create(
1113 comment = CommentsModel().create(
1114 text=text,
1114 text=text,
1115 repo=repo.repo_id,
1115 repo=repo.repo_id,
1116 user=user.user_id,
1116 user=user.user_id,
1117 pull_request=pull_request.pull_request_id,
1117 pull_request=pull_request.pull_request_id,
1118 status_change=status_lbl,
1118 status_change=status_lbl,
1119 status_change_type=status,
1119 status_change_type=status,
1120 closing_pr=True
1120 closing_pr=True
1121 )
1121 )
1122
1122
1123 # calculate old status before we change it
1123 # calculate old status before we change it
1124 old_calculated_status = pull_request.calculated_review_status()
1124 old_calculated_status = pull_request.calculated_review_status()
1125 ChangesetStatusModel().set_status(
1125 ChangesetStatusModel().set_status(
1126 repo.repo_id,
1126 repo.repo_id,
1127 status,
1127 status,
1128 user.user_id,
1128 user.user_id,
1129 comment=comment,
1129 comment=comment,
1130 pull_request=pull_request.pull_request_id
1130 pull_request=pull_request.pull_request_id
1131 )
1131 )
1132
1132
1133 Session().flush()
1133 Session().flush()
1134 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1134 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1135 # we now calculate the status of pull request again, and based on that
1135 # we now calculate the status of pull request again, and based on that
1136 # calculation trigger status change. This might happen in cases
1136 # calculation trigger status change. This might happen in cases
1137 # that non-reviewer admin closes a pr, which means his vote doesn't
1137 # that non-reviewer admin closes a pr, which means his vote doesn't
1138 # change the status, while if he's a reviewer this might change it.
1138 # change the status, while if he's a reviewer this might change it.
1139 calculated_status = pull_request.calculated_review_status()
1139 calculated_status = pull_request.calculated_review_status()
1140 if old_calculated_status != calculated_status:
1140 if old_calculated_status != calculated_status:
1141 self._trigger_pull_request_hook(
1141 self._trigger_pull_request_hook(
1142 pull_request, user, 'review_status_change')
1142 pull_request, user, 'review_status_change')
1143
1143
1144 # finally close the PR
1144 # finally close the PR
1145 PullRequestModel().close_pull_request(
1145 PullRequestModel().close_pull_request(
1146 pull_request.pull_request_id, user)
1146 pull_request.pull_request_id, user)
1147
1147
1148 return comment, status
1148 return comment, status
1149
1149
1150 def merge_status(self, pull_request):
1150 def merge_status(self, pull_request):
1151 if not self._is_merge_enabled(pull_request):
1151 if not self._is_merge_enabled(pull_request):
1152 return False, _('Server-side pull request merging is disabled.')
1152 return False, _('Server-side pull request merging is disabled.')
1153 if pull_request.is_closed():
1153 if pull_request.is_closed():
1154 return False, _('This pull request is closed.')
1154 return False, _('This pull request is closed.')
1155 merge_possible, msg = self._check_repo_requirements(
1155 merge_possible, msg = self._check_repo_requirements(
1156 target=pull_request.target_repo, source=pull_request.source_repo)
1156 target=pull_request.target_repo, source=pull_request.source_repo)
1157 if not merge_possible:
1157 if not merge_possible:
1158 return merge_possible, msg
1158 return merge_possible, msg
1159
1159
1160 try:
1160 try:
1161 resp = self._try_merge(pull_request)
1161 resp = self._try_merge(pull_request)
1162 log.debug("Merge response: %s", resp)
1162 log.debug("Merge response: %s", resp)
1163 status = resp.possible, self.merge_status_message(
1163 status = resp.possible, self.merge_status_message(
1164 resp.failure_reason)
1164 resp.failure_reason)
1165 except NotImplementedError:
1165 except NotImplementedError:
1166 status = False, _('Pull request merging is not supported.')
1166 status = False, _('Pull request merging is not supported.')
1167
1167
1168 return status
1168 return status
1169
1169
1170 def _check_repo_requirements(self, target, source):
1170 def _check_repo_requirements(self, target, source):
1171 """
1171 """
1172 Check if `target` and `source` have compatible requirements.
1172 Check if `target` and `source` have compatible requirements.
1173
1173
1174 Currently this is just checking for largefiles.
1174 Currently this is just checking for largefiles.
1175 """
1175 """
1176 target_has_largefiles = self._has_largefiles(target)
1176 target_has_largefiles = self._has_largefiles(target)
1177 source_has_largefiles = self._has_largefiles(source)
1177 source_has_largefiles = self._has_largefiles(source)
1178 merge_possible = True
1178 merge_possible = True
1179 message = u''
1179 message = u''
1180
1180
1181 if target_has_largefiles != source_has_largefiles:
1181 if target_has_largefiles != source_has_largefiles:
1182 merge_possible = False
1182 merge_possible = False
1183 if source_has_largefiles:
1183 if source_has_largefiles:
1184 message = _(
1184 message = _(
1185 'Target repository large files support is disabled.')
1185 'Target repository large files support is disabled.')
1186 else:
1186 else:
1187 message = _(
1187 message = _(
1188 'Source repository large files support is disabled.')
1188 'Source repository large files support is disabled.')
1189
1189
1190 return merge_possible, message
1190 return merge_possible, message
1191
1191
1192 def _has_largefiles(self, repo):
1192 def _has_largefiles(self, repo):
1193 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1193 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1194 'extensions', 'largefiles')
1194 'extensions', 'largefiles')
1195 return largefiles_ui and largefiles_ui[0].active
1195 return largefiles_ui and largefiles_ui[0].active
1196
1196
1197 def _try_merge(self, pull_request):
1197 def _try_merge(self, pull_request):
1198 """
1198 """
1199 Try to merge the pull request and return the merge status.
1199 Try to merge the pull request and return the merge status.
1200 """
1200 """
1201 log.debug(
1201 log.debug(
1202 "Trying out if the pull request %s can be merged.",
1202 "Trying out if the pull request %s can be merged.",
1203 pull_request.pull_request_id)
1203 pull_request.pull_request_id)
1204 target_vcs = pull_request.target_repo.scm_instance()
1204 target_vcs = pull_request.target_repo.scm_instance()
1205
1205
1206 # Refresh the target reference.
1206 # Refresh the target reference.
1207 try:
1207 try:
1208 target_ref = self._refresh_reference(
1208 target_ref = self._refresh_reference(
1209 pull_request.target_ref_parts, target_vcs)
1209 pull_request.target_ref_parts, target_vcs)
1210 except CommitDoesNotExistError:
1210 except CommitDoesNotExistError:
1211 merge_state = MergeResponse(
1211 merge_state = MergeResponse(
1212 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1212 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1213 return merge_state
1213 return merge_state
1214
1214
1215 target_locked = pull_request.target_repo.locked
1215 target_locked = pull_request.target_repo.locked
1216 if target_locked and target_locked[0]:
1216 if target_locked and target_locked[0]:
1217 log.debug("The target repository is locked.")
1217 log.debug("The target repository is locked.")
1218 merge_state = MergeResponse(
1218 merge_state = MergeResponse(
1219 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1219 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1220 elif self._needs_merge_state_refresh(pull_request, target_ref):
1220 elif self._needs_merge_state_refresh(pull_request, target_ref):
1221 log.debug("Refreshing the merge status of the repository.")
1221 log.debug("Refreshing the merge status of the repository.")
1222 merge_state = self._refresh_merge_state(
1222 merge_state = self._refresh_merge_state(
1223 pull_request, target_vcs, target_ref)
1223 pull_request, target_vcs, target_ref)
1224 else:
1224 else:
1225 possible = pull_request.\
1225 possible = pull_request.\
1226 _last_merge_status == MergeFailureReason.NONE
1226 last_merge_status == MergeFailureReason.NONE
1227 merge_state = MergeResponse(
1227 merge_state = MergeResponse(
1228 possible, False, None, pull_request._last_merge_status)
1228 possible, False, None, pull_request.last_merge_status)
1229
1229
1230 return merge_state
1230 return merge_state
1231
1231
1232 def _refresh_reference(self, reference, vcs_repository):
1232 def _refresh_reference(self, reference, vcs_repository):
1233 if reference.type in ('branch', 'book'):
1233 if reference.type in ('branch', 'book'):
1234 name_or_id = reference.name
1234 name_or_id = reference.name
1235 else:
1235 else:
1236 name_or_id = reference.commit_id
1236 name_or_id = reference.commit_id
1237 refreshed_commit = vcs_repository.get_commit(name_or_id)
1237 refreshed_commit = vcs_repository.get_commit(name_or_id)
1238 refreshed_reference = Reference(
1238 refreshed_reference = Reference(
1239 reference.type, reference.name, refreshed_commit.raw_id)
1239 reference.type, reference.name, refreshed_commit.raw_id)
1240 return refreshed_reference
1240 return refreshed_reference
1241
1241
1242 def _needs_merge_state_refresh(self, pull_request, target_reference):
1242 def _needs_merge_state_refresh(self, pull_request, target_reference):
1243 return not(
1243 return not(
1244 pull_request.revisions and
1244 pull_request.revisions and
1245 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1245 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1246 target_reference.commit_id == pull_request._last_merge_target_rev)
1246 target_reference.commit_id == pull_request._last_merge_target_rev)
1247
1247
1248 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1248 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1249 workspace_id = self._workspace_id(pull_request)
1249 workspace_id = self._workspace_id(pull_request)
1250 source_vcs = pull_request.source_repo.scm_instance()
1250 source_vcs = pull_request.source_repo.scm_instance()
1251 use_rebase = self._use_rebase_for_merging(pull_request)
1251 use_rebase = self._use_rebase_for_merging(pull_request)
1252 merge_state = target_vcs.merge(
1252 merge_state = target_vcs.merge(
1253 target_reference, source_vcs, pull_request.source_ref_parts,
1253 target_reference, source_vcs, pull_request.source_ref_parts,
1254 workspace_id, dry_run=True, use_rebase=use_rebase)
1254 workspace_id, dry_run=True, use_rebase=use_rebase)
1255
1255
1256 # Do not store the response if there was an unknown error.
1256 # Do not store the response if there was an unknown error.
1257 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1257 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1258 pull_request._last_merge_source_rev = \
1258 pull_request._last_merge_source_rev = \
1259 pull_request.source_ref_parts.commit_id
1259 pull_request.source_ref_parts.commit_id
1260 pull_request._last_merge_target_rev = target_reference.commit_id
1260 pull_request._last_merge_target_rev = target_reference.commit_id
1261 pull_request._last_merge_status = merge_state.failure_reason
1261 pull_request.last_merge_status = merge_state.failure_reason
1262 pull_request.shadow_merge_ref = merge_state.merge_ref
1262 pull_request.shadow_merge_ref = merge_state.merge_ref
1263 Session().add(pull_request)
1263 Session().add(pull_request)
1264 Session().commit()
1264 Session().commit()
1265
1265
1266 return merge_state
1266 return merge_state
1267
1267
1268 def _workspace_id(self, pull_request):
1268 def _workspace_id(self, pull_request):
1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1270 return workspace_id
1270 return workspace_id
1271
1271
1272 def merge_status_message(self, status_code):
1272 def merge_status_message(self, status_code):
1273 """
1273 """
1274 Return a human friendly error message for the given merge status code.
1274 Return a human friendly error message for the given merge status code.
1275 """
1275 """
1276 return self.MERGE_STATUS_MESSAGES[status_code]
1276 return self.MERGE_STATUS_MESSAGES[status_code]
1277
1277
1278 def generate_repo_data(self, repo, commit_id=None, branch=None,
1278 def generate_repo_data(self, repo, commit_id=None, branch=None,
1279 bookmark=None):
1279 bookmark=None):
1280 all_refs, selected_ref = \
1280 all_refs, selected_ref = \
1281 self._get_repo_pullrequest_sources(
1281 self._get_repo_pullrequest_sources(
1282 repo.scm_instance(), commit_id=commit_id,
1282 repo.scm_instance(), commit_id=commit_id,
1283 branch=branch, bookmark=bookmark)
1283 branch=branch, bookmark=bookmark)
1284
1284
1285 refs_select2 = []
1285 refs_select2 = []
1286 for element in all_refs:
1286 for element in all_refs:
1287 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1287 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1288 refs_select2.append({'text': element[1], 'children': children})
1288 refs_select2.append({'text': element[1], 'children': children})
1289
1289
1290 return {
1290 return {
1291 'user': {
1291 'user': {
1292 'user_id': repo.user.user_id,
1292 'user_id': repo.user.user_id,
1293 'username': repo.user.username,
1293 'username': repo.user.username,
1294 'firstname': repo.user.first_name,
1294 'firstname': repo.user.first_name,
1295 'lastname': repo.user.last_name,
1295 'lastname': repo.user.last_name,
1296 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1296 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1297 },
1297 },
1298 'description': h.chop_at_smart(repo.description_safe, '\n'),
1298 'description': h.chop_at_smart(repo.description_safe, '\n'),
1299 'refs': {
1299 'refs': {
1300 'all_refs': all_refs,
1300 'all_refs': all_refs,
1301 'selected_ref': selected_ref,
1301 'selected_ref': selected_ref,
1302 'select2_refs': refs_select2
1302 'select2_refs': refs_select2
1303 }
1303 }
1304 }
1304 }
1305
1305
1306 def generate_pullrequest_title(self, source, source_ref, target):
1306 def generate_pullrequest_title(self, source, source_ref, target):
1307 return u'{source}#{at_ref} to {target}'.format(
1307 return u'{source}#{at_ref} to {target}'.format(
1308 source=source,
1308 source=source,
1309 at_ref=source_ref,
1309 at_ref=source_ref,
1310 target=target,
1310 target=target,
1311 )
1311 )
1312
1312
1313 def _cleanup_merge_workspace(self, pull_request):
1313 def _cleanup_merge_workspace(self, pull_request):
1314 # Merging related cleanup
1314 # Merging related cleanup
1315 target_scm = pull_request.target_repo.scm_instance()
1315 target_scm = pull_request.target_repo.scm_instance()
1316 workspace_id = 'pr-%s' % pull_request.pull_request_id
1316 workspace_id = 'pr-%s' % pull_request.pull_request_id
1317
1317
1318 try:
1318 try:
1319 target_scm.cleanup_merge_workspace(workspace_id)
1319 target_scm.cleanup_merge_workspace(workspace_id)
1320 except NotImplementedError:
1320 except NotImplementedError:
1321 pass
1321 pass
1322
1322
1323 def _get_repo_pullrequest_sources(
1323 def _get_repo_pullrequest_sources(
1324 self, repo, commit_id=None, branch=None, bookmark=None):
1324 self, repo, commit_id=None, branch=None, bookmark=None):
1325 """
1325 """
1326 Return a structure with repo's interesting commits, suitable for
1326 Return a structure with repo's interesting commits, suitable for
1327 the selectors in pullrequest controller
1327 the selectors in pullrequest controller
1328
1328
1329 :param commit_id: a commit that must be in the list somehow
1329 :param commit_id: a commit that must be in the list somehow
1330 and selected by default
1330 and selected by default
1331 :param branch: a branch that must be in the list and selected
1331 :param branch: a branch that must be in the list and selected
1332 by default - even if closed
1332 by default - even if closed
1333 :param bookmark: a bookmark that must be in the list and selected
1333 :param bookmark: a bookmark that must be in the list and selected
1334 """
1334 """
1335
1335
1336 commit_id = safe_str(commit_id) if commit_id else None
1336 commit_id = safe_str(commit_id) if commit_id else None
1337 branch = safe_str(branch) if branch else None
1337 branch = safe_str(branch) if branch else None
1338 bookmark = safe_str(bookmark) if bookmark else None
1338 bookmark = safe_str(bookmark) if bookmark else None
1339
1339
1340 selected = None
1340 selected = None
1341
1341
1342 # order matters: first source that has commit_id in it will be selected
1342 # order matters: first source that has commit_id in it will be selected
1343 sources = []
1343 sources = []
1344 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1344 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1345 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1345 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1346
1346
1347 if commit_id:
1347 if commit_id:
1348 ref_commit = (h.short_id(commit_id), commit_id)
1348 ref_commit = (h.short_id(commit_id), commit_id)
1349 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1349 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1350
1350
1351 sources.append(
1351 sources.append(
1352 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1352 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1353 )
1353 )
1354
1354
1355 groups = []
1355 groups = []
1356 for group_key, ref_list, group_name, match in sources:
1356 for group_key, ref_list, group_name, match in sources:
1357 group_refs = []
1357 group_refs = []
1358 for ref_name, ref_id in ref_list:
1358 for ref_name, ref_id in ref_list:
1359 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1359 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1360 group_refs.append((ref_key, ref_name))
1360 group_refs.append((ref_key, ref_name))
1361
1361
1362 if not selected:
1362 if not selected:
1363 if set([commit_id, match]) & set([ref_id, ref_name]):
1363 if set([commit_id, match]) & set([ref_id, ref_name]):
1364 selected = ref_key
1364 selected = ref_key
1365
1365
1366 if group_refs:
1366 if group_refs:
1367 groups.append((group_refs, group_name))
1367 groups.append((group_refs, group_name))
1368
1368
1369 if not selected:
1369 if not selected:
1370 ref = commit_id or branch or bookmark
1370 ref = commit_id or branch or bookmark
1371 if ref:
1371 if ref:
1372 raise CommitDoesNotExistError(
1372 raise CommitDoesNotExistError(
1373 'No commit refs could be found matching: %s' % ref)
1373 'No commit refs could be found matching: %s' % ref)
1374 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1374 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1375 selected = 'branch:%s:%s' % (
1375 selected = 'branch:%s:%s' % (
1376 repo.DEFAULT_BRANCH_NAME,
1376 repo.DEFAULT_BRANCH_NAME,
1377 repo.branches[repo.DEFAULT_BRANCH_NAME]
1377 repo.branches[repo.DEFAULT_BRANCH_NAME]
1378 )
1378 )
1379 elif repo.commit_ids:
1379 elif repo.commit_ids:
1380 rev = repo.commit_ids[0]
1380 rev = repo.commit_ids[0]
1381 selected = 'rev:%s:%s' % (rev, rev)
1381 selected = 'rev:%s:%s' % (rev, rev)
1382 else:
1382 else:
1383 raise EmptyRepositoryError()
1383 raise EmptyRepositoryError()
1384 return groups, selected
1384 return groups, selected
1385
1385
1386 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1386 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1387 return self._get_diff_from_pr_or_version(
1387 return self._get_diff_from_pr_or_version(
1388 source_repo, source_ref_id, target_ref_id, context=context)
1388 source_repo, source_ref_id, target_ref_id, context=context)
1389
1389
1390 def _get_diff_from_pr_or_version(
1390 def _get_diff_from_pr_or_version(
1391 self, source_repo, source_ref_id, target_ref_id, context):
1391 self, source_repo, source_ref_id, target_ref_id, context):
1392 target_commit = source_repo.get_commit(
1392 target_commit = source_repo.get_commit(
1393 commit_id=safe_str(target_ref_id))
1393 commit_id=safe_str(target_ref_id))
1394 source_commit = source_repo.get_commit(
1394 source_commit = source_repo.get_commit(
1395 commit_id=safe_str(source_ref_id))
1395 commit_id=safe_str(source_ref_id))
1396 if isinstance(source_repo, Repository):
1396 if isinstance(source_repo, Repository):
1397 vcs_repo = source_repo.scm_instance()
1397 vcs_repo = source_repo.scm_instance()
1398 else:
1398 else:
1399 vcs_repo = source_repo
1399 vcs_repo = source_repo
1400
1400
1401 # TODO: johbo: In the context of an update, we cannot reach
1401 # TODO: johbo: In the context of an update, we cannot reach
1402 # the old commit anymore with our normal mechanisms. It needs
1402 # the old commit anymore with our normal mechanisms. It needs
1403 # some sort of special support in the vcs layer to avoid this
1403 # some sort of special support in the vcs layer to avoid this
1404 # workaround.
1404 # workaround.
1405 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1405 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1406 vcs_repo.alias == 'git'):
1406 vcs_repo.alias == 'git'):
1407 source_commit.raw_id = safe_str(source_ref_id)
1407 source_commit.raw_id = safe_str(source_ref_id)
1408
1408
1409 log.debug('calculating diff between '
1409 log.debug('calculating diff between '
1410 'source_ref:%s and target_ref:%s for repo `%s`',
1410 'source_ref:%s and target_ref:%s for repo `%s`',
1411 target_ref_id, source_ref_id,
1411 target_ref_id, source_ref_id,
1412 safe_unicode(vcs_repo.path))
1412 safe_unicode(vcs_repo.path))
1413
1413
1414 vcs_diff = vcs_repo.get_diff(
1414 vcs_diff = vcs_repo.get_diff(
1415 commit1=target_commit, commit2=source_commit, context=context)
1415 commit1=target_commit, commit2=source_commit, context=context)
1416 return vcs_diff
1416 return vcs_diff
1417
1417
1418 def _is_merge_enabled(self, pull_request):
1418 def _is_merge_enabled(self, pull_request):
1419 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1419 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1420 settings = settings_model.get_general_settings()
1420 settings = settings_model.get_general_settings()
1421 return settings.get('rhodecode_pr_merge_enabled', False)
1421 return settings.get('rhodecode_pr_merge_enabled', False)
1422
1422
1423 def _use_rebase_for_merging(self, pull_request):
1423 def _use_rebase_for_merging(self, pull_request):
1424 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1424 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1425 settings = settings_model.get_general_settings()
1425 settings = settings_model.get_general_settings()
1426 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1426 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1427
1427
1428 def _log_audit_action(self, action, action_data, user, pull_request):
1428 def _log_audit_action(self, action, action_data, user, pull_request):
1429 audit_logger.store(
1429 audit_logger.store(
1430 action=action,
1430 action=action,
1431 action_data=action_data,
1431 action_data=action_data,
1432 user=user,
1432 user=user,
1433 repo=pull_request.target_repo)
1433 repo=pull_request.target_repo)
1434
1434
1435 def get_reviewer_functions(self):
1435 def get_reviewer_functions(self):
1436 """
1436 """
1437 Fetches functions for validation and fetching default reviewers.
1437 Fetches functions for validation and fetching default reviewers.
1438 If available we use the EE package, else we fallback to CE
1438 If available we use the EE package, else we fallback to CE
1439 package functions
1439 package functions
1440 """
1440 """
1441 try:
1441 try:
1442 from rc_reviewers.utils import get_default_reviewers_data
1442 from rc_reviewers.utils import get_default_reviewers_data
1443 from rc_reviewers.utils import validate_default_reviewers
1443 from rc_reviewers.utils import validate_default_reviewers
1444 except ImportError:
1444 except ImportError:
1445 from rhodecode.apps.repository.utils import \
1445 from rhodecode.apps.repository.utils import \
1446 get_default_reviewers_data
1446 get_default_reviewers_data
1447 from rhodecode.apps.repository.utils import \
1447 from rhodecode.apps.repository.utils import \
1448 validate_default_reviewers
1448 validate_default_reviewers
1449
1449
1450 return get_default_reviewers_data, validate_default_reviewers
1450 return get_default_reviewers_data, validate_default_reviewers
1451
1451
1452
1452
1453 class MergeCheck(object):
1453 class MergeCheck(object):
1454 """
1454 """
1455 Perform Merge Checks and returns a check object which stores information
1455 Perform Merge Checks and returns a check object which stores information
1456 about merge errors, and merge conditions
1456 about merge errors, and merge conditions
1457 """
1457 """
1458 TODO_CHECK = 'todo'
1458 TODO_CHECK = 'todo'
1459 PERM_CHECK = 'perm'
1459 PERM_CHECK = 'perm'
1460 REVIEW_CHECK = 'review'
1460 REVIEW_CHECK = 'review'
1461 MERGE_CHECK = 'merge'
1461 MERGE_CHECK = 'merge'
1462
1462
1463 def __init__(self):
1463 def __init__(self):
1464 self.review_status = None
1464 self.review_status = None
1465 self.merge_possible = None
1465 self.merge_possible = None
1466 self.merge_msg = ''
1466 self.merge_msg = ''
1467 self.failed = None
1467 self.failed = None
1468 self.errors = []
1468 self.errors = []
1469 self.error_details = OrderedDict()
1469 self.error_details = OrderedDict()
1470
1470
1471 def push_error(self, error_type, message, error_key, details):
1471 def push_error(self, error_type, message, error_key, details):
1472 self.failed = True
1472 self.failed = True
1473 self.errors.append([error_type, message])
1473 self.errors.append([error_type, message])
1474 self.error_details[error_key] = dict(
1474 self.error_details[error_key] = dict(
1475 details=details,
1475 details=details,
1476 error_type=error_type,
1476 error_type=error_type,
1477 message=message
1477 message=message
1478 )
1478 )
1479
1479
1480 @classmethod
1480 @classmethod
1481 def validate(cls, pull_request, user, fail_early=False, translator=None):
1481 def validate(cls, pull_request, user, fail_early=False, translator=None):
1482 # if migrated to pyramid...
1482 # if migrated to pyramid...
1483 # _ = lambda: translator or _ # use passed in translator if any
1483 # _ = lambda: translator or _ # use passed in translator if any
1484
1484
1485 merge_check = cls()
1485 merge_check = cls()
1486
1486
1487 # permissions to merge
1487 # permissions to merge
1488 user_allowed_to_merge = PullRequestModel().check_user_merge(
1488 user_allowed_to_merge = PullRequestModel().check_user_merge(
1489 pull_request, user)
1489 pull_request, user)
1490 if not user_allowed_to_merge:
1490 if not user_allowed_to_merge:
1491 log.debug("MergeCheck: cannot merge, approval is pending.")
1491 log.debug("MergeCheck: cannot merge, approval is pending.")
1492
1492
1493 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1493 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1494 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1494 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1495 if fail_early:
1495 if fail_early:
1496 return merge_check
1496 return merge_check
1497
1497
1498 # review status, must be always present
1498 # review status, must be always present
1499 review_status = pull_request.calculated_review_status()
1499 review_status = pull_request.calculated_review_status()
1500 merge_check.review_status = review_status
1500 merge_check.review_status = review_status
1501
1501
1502 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1502 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1503 if not status_approved:
1503 if not status_approved:
1504 log.debug("MergeCheck: cannot merge, approval is pending.")
1504 log.debug("MergeCheck: cannot merge, approval is pending.")
1505
1505
1506 msg = _('Pull request reviewer approval is pending.')
1506 msg = _('Pull request reviewer approval is pending.')
1507
1507
1508 merge_check.push_error(
1508 merge_check.push_error(
1509 'warning', msg, cls.REVIEW_CHECK, review_status)
1509 'warning', msg, cls.REVIEW_CHECK, review_status)
1510
1510
1511 if fail_early:
1511 if fail_early:
1512 return merge_check
1512 return merge_check
1513
1513
1514 # left over TODOs
1514 # left over TODOs
1515 todos = CommentsModel().get_unresolved_todos(pull_request)
1515 todos = CommentsModel().get_unresolved_todos(pull_request)
1516 if todos:
1516 if todos:
1517 log.debug("MergeCheck: cannot merge, {} "
1517 log.debug("MergeCheck: cannot merge, {} "
1518 "unresolved todos left.".format(len(todos)))
1518 "unresolved todos left.".format(len(todos)))
1519
1519
1520 if len(todos) == 1:
1520 if len(todos) == 1:
1521 msg = _('Cannot merge, {} TODO still not resolved.').format(
1521 msg = _('Cannot merge, {} TODO still not resolved.').format(
1522 len(todos))
1522 len(todos))
1523 else:
1523 else:
1524 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1524 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1525 len(todos))
1525 len(todos))
1526
1526
1527 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1527 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1528
1528
1529 if fail_early:
1529 if fail_early:
1530 return merge_check
1530 return merge_check
1531
1531
1532 # merge possible
1532 # merge possible
1533 merge_status, msg = PullRequestModel().merge_status(pull_request)
1533 merge_status, msg = PullRequestModel().merge_status(pull_request)
1534 merge_check.merge_possible = merge_status
1534 merge_check.merge_possible = merge_status
1535 merge_check.merge_msg = msg
1535 merge_check.merge_msg = msg
1536 if not merge_status:
1536 if not merge_status:
1537 log.debug(
1537 log.debug(
1538 "MergeCheck: cannot merge, pull request merge not possible.")
1538 "MergeCheck: cannot merge, pull request merge not possible.")
1539 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1539 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1540
1540
1541 if fail_early:
1541 if fail_early:
1542 return merge_check
1542 return merge_check
1543
1543
1544 return merge_check
1544 return merge_check
1545
1545
1546
1546
1547 ChangeTuple = namedtuple('ChangeTuple',
1547 ChangeTuple = namedtuple('ChangeTuple',
1548 ['added', 'common', 'removed', 'total'])
1548 ['added', 'common', 'removed', 'total'])
1549
1549
1550 FileChangeTuple = namedtuple('FileChangeTuple',
1550 FileChangeTuple = namedtuple('FileChangeTuple',
1551 ['added', 'modified', 'removed'])
1551 ['added', 'modified', 'removed'])
@@ -1,859 +1,859 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import CommentsModel
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 @pytest.mark.usefixtures('config_stub')
44 @pytest.mark.usefixtures('config_stub')
45 class TestPullRequestModel(object):
45 class TestPullRequestModel(object):
46
46
47 @pytest.fixture
47 @pytest.fixture
48 def pull_request(self, request, backend, pr_util):
48 def pull_request(self, request, backend, pr_util):
49 """
49 """
50 A pull request combined with multiples patches.
50 A pull request combined with multiples patches.
51 """
51 """
52 BackendClass = get_backend(backend.alias)
52 BackendClass = get_backend(backend.alias)
53 self.merge_patcher = mock.patch.object(
53 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
55 False, False, None, MergeFailureReason.UNKNOWN))
56 self.workspace_remove_patcher = mock.patch.object(
56 self.workspace_remove_patcher = mock.patch.object(
57 BackendClass, 'cleanup_merge_workspace')
57 BackendClass, 'cleanup_merge_workspace')
58
58
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
61 self.comment_patcher = mock.patch(
61 self.comment_patcher = mock.patch(
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
63 self.comment_patcher.start()
63 self.comment_patcher.start()
64 self.notification_patcher = mock.patch(
64 self.notification_patcher = mock.patch(
65 'rhodecode.model.notification.NotificationModel.create')
65 'rhodecode.model.notification.NotificationModel.create')
66 self.notification_patcher.start()
66 self.notification_patcher.start()
67 self.helper_patcher = mock.patch(
67 self.helper_patcher = mock.patch(
68 'rhodecode.lib.helpers.url')
68 'rhodecode.lib.helpers.url')
69 self.helper_patcher.start()
69 self.helper_patcher.start()
70
70
71 self.hook_patcher = mock.patch.object(PullRequestModel,
71 self.hook_patcher = mock.patch.object(PullRequestModel,
72 '_trigger_pull_request_hook')
72 '_trigger_pull_request_hook')
73 self.hook_mock = self.hook_patcher.start()
73 self.hook_mock = self.hook_patcher.start()
74
74
75 self.invalidation_patcher = mock.patch(
75 self.invalidation_patcher = mock.patch(
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
77 self.invalidation_mock = self.invalidation_patcher.start()
77 self.invalidation_mock = self.invalidation_patcher.start()
78
78
79 self.pull_request = pr_util.create_pull_request(
79 self.pull_request = pr_util.create_pull_request(
80 mergeable=True, name_suffix=u'Δ…Δ‡')
80 mergeable=True, name_suffix=u'Δ…Δ‡')
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
84
84
85 @request.addfinalizer
85 @request.addfinalizer
86 def cleanup_pull_request():
86 def cleanup_pull_request():
87 calls = [mock.call(
87 calls = [mock.call(
88 self.pull_request, self.pull_request.author, 'create')]
88 self.pull_request, self.pull_request.author, 'create')]
89 self.hook_mock.assert_has_calls(calls)
89 self.hook_mock.assert_has_calls(calls)
90
90
91 self.workspace_remove_patcher.stop()
91 self.workspace_remove_patcher.stop()
92 self.merge_patcher.stop()
92 self.merge_patcher.stop()
93 self.comment_patcher.stop()
93 self.comment_patcher.stop()
94 self.notification_patcher.stop()
94 self.notification_patcher.stop()
95 self.helper_patcher.stop()
95 self.helper_patcher.stop()
96 self.hook_patcher.stop()
96 self.hook_patcher.stop()
97 self.invalidation_patcher.stop()
97 self.invalidation_patcher.stop()
98
98
99 return self.pull_request
99 return self.pull_request
100
100
101 def test_get_all(self, pull_request):
101 def test_get_all(self, pull_request):
102 prs = PullRequestModel().get_all(pull_request.target_repo)
102 prs = PullRequestModel().get_all(pull_request.target_repo)
103 assert isinstance(prs, list)
103 assert isinstance(prs, list)
104 assert len(prs) == 1
104 assert len(prs) == 1
105
105
106 def test_count_all(self, pull_request):
106 def test_count_all(self, pull_request):
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
108 assert pr_count == 1
108 assert pr_count == 1
109
109
110 def test_get_awaiting_review(self, pull_request):
110 def test_get_awaiting_review(self, pull_request):
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
112 assert isinstance(prs, list)
112 assert isinstance(prs, list)
113 assert len(prs) == 1
113 assert len(prs) == 1
114
114
115 def test_count_awaiting_review(self, pull_request):
115 def test_count_awaiting_review(self, pull_request):
116 pr_count = PullRequestModel().count_awaiting_review(
116 pr_count = PullRequestModel().count_awaiting_review(
117 pull_request.target_repo)
117 pull_request.target_repo)
118 assert pr_count == 1
118 assert pr_count == 1
119
119
120 def test_get_awaiting_my_review(self, pull_request):
120 def test_get_awaiting_my_review(self, pull_request):
121 PullRequestModel().update_reviewers(
121 PullRequestModel().update_reviewers(
122 pull_request, [(pull_request.author, ['author'], False)],
122 pull_request, [(pull_request.author, ['author'], False)],
123 pull_request.author)
123 pull_request.author)
124 prs = PullRequestModel().get_awaiting_my_review(
124 prs = PullRequestModel().get_awaiting_my_review(
125 pull_request.target_repo, user_id=pull_request.author.user_id)
125 pull_request.target_repo, user_id=pull_request.author.user_id)
126 assert isinstance(prs, list)
126 assert isinstance(prs, list)
127 assert len(prs) == 1
127 assert len(prs) == 1
128
128
129 def test_count_awaiting_my_review(self, pull_request):
129 def test_count_awaiting_my_review(self, pull_request):
130 PullRequestModel().update_reviewers(
130 PullRequestModel().update_reviewers(
131 pull_request, [(pull_request.author, ['author'], False)],
131 pull_request, [(pull_request.author, ['author'], False)],
132 pull_request.author)
132 pull_request.author)
133 pr_count = PullRequestModel().count_awaiting_my_review(
133 pr_count = PullRequestModel().count_awaiting_my_review(
134 pull_request.target_repo, user_id=pull_request.author.user_id)
134 pull_request.target_repo, user_id=pull_request.author.user_id)
135 assert pr_count == 1
135 assert pr_count == 1
136
136
137 def test_delete_calls_cleanup_merge(self, pull_request):
137 def test_delete_calls_cleanup_merge(self, pull_request):
138 PullRequestModel().delete(pull_request, pull_request.author)
138 PullRequestModel().delete(pull_request, pull_request.author)
139
139
140 self.workspace_remove_mock.assert_called_once_with(
140 self.workspace_remove_mock.assert_called_once_with(
141 self.workspace_id)
141 self.workspace_id)
142
142
143 def test_close_calls_cleanup_and_hook(self, pull_request):
143 def test_close_calls_cleanup_and_hook(self, pull_request):
144 PullRequestModel().close_pull_request(
144 PullRequestModel().close_pull_request(
145 pull_request, pull_request.author)
145 pull_request, pull_request.author)
146
146
147 self.workspace_remove_mock.assert_called_once_with(
147 self.workspace_remove_mock.assert_called_once_with(
148 self.workspace_id)
148 self.workspace_id)
149 self.hook_mock.assert_called_with(
149 self.hook_mock.assert_called_with(
150 self.pull_request, self.pull_request.author, 'close')
150 self.pull_request, self.pull_request.author, 'close')
151
151
152 def test_merge_status(self, pull_request):
152 def test_merge_status(self, pull_request):
153 self.merge_mock.return_value = MergeResponse(
153 self.merge_mock.return_value = MergeResponse(
154 True, False, None, MergeFailureReason.NONE)
154 True, False, None, MergeFailureReason.NONE)
155
155
156 assert pull_request._last_merge_source_rev is None
156 assert pull_request._last_merge_source_rev is None
157 assert pull_request._last_merge_target_rev is None
157 assert pull_request._last_merge_target_rev is None
158 assert pull_request._last_merge_status is None
158 assert pull_request.last_merge_status is None
159
159
160 status, msg = PullRequestModel().merge_status(pull_request)
160 status, msg = PullRequestModel().merge_status(pull_request)
161 assert status is True
161 assert status is True
162 assert msg.eval() == 'This pull request can be automatically merged.'
162 assert msg.eval() == 'This pull request can be automatically merged.'
163 self.merge_mock.assert_called_once_with(
163 self.merge_mock.assert_called_once_with(
164 pull_request.target_ref_parts,
164 pull_request.target_ref_parts,
165 pull_request.source_repo.scm_instance(),
165 pull_request.source_repo.scm_instance(),
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
167 use_rebase=False)
167 use_rebase=False)
168
168
169 assert pull_request._last_merge_source_rev == self.source_commit
169 assert pull_request._last_merge_source_rev == self.source_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
171 assert pull_request._last_merge_status is MergeFailureReason.NONE
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
172
172
173 self.merge_mock.reset_mock()
173 self.merge_mock.reset_mock()
174 status, msg = PullRequestModel().merge_status(pull_request)
174 status, msg = PullRequestModel().merge_status(pull_request)
175 assert status is True
175 assert status is True
176 assert msg.eval() == 'This pull request can be automatically merged.'
176 assert msg.eval() == 'This pull request can be automatically merged.'
177 assert self.merge_mock.called is False
177 assert self.merge_mock.called is False
178
178
179 def test_merge_status_known_failure(self, pull_request):
179 def test_merge_status_known_failure(self, pull_request):
180 self.merge_mock.return_value = MergeResponse(
180 self.merge_mock.return_value = MergeResponse(
181 False, False, None, MergeFailureReason.MERGE_FAILED)
181 False, False, None, MergeFailureReason.MERGE_FAILED)
182
182
183 assert pull_request._last_merge_source_rev is None
183 assert pull_request._last_merge_source_rev is None
184 assert pull_request._last_merge_target_rev is None
184 assert pull_request._last_merge_target_rev is None
185 assert pull_request._last_merge_status is None
185 assert pull_request.last_merge_status is None
186
186
187 status, msg = PullRequestModel().merge_status(pull_request)
187 status, msg = PullRequestModel().merge_status(pull_request)
188 assert status is False
188 assert status is False
189 assert (
189 assert (
190 msg.eval() ==
190 msg.eval() ==
191 'This pull request cannot be merged because of merge conflicts.')
191 'This pull request cannot be merged because of merge conflicts.')
192 self.merge_mock.assert_called_once_with(
192 self.merge_mock.assert_called_once_with(
193 pull_request.target_ref_parts,
193 pull_request.target_ref_parts,
194 pull_request.source_repo.scm_instance(),
194 pull_request.source_repo.scm_instance(),
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
196 use_rebase=False)
196 use_rebase=False)
197
197
198 assert pull_request._last_merge_source_rev == self.source_commit
198 assert pull_request._last_merge_source_rev == self.source_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
200 assert (
200 assert (
201 pull_request._last_merge_status is MergeFailureReason.MERGE_FAILED)
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
202
202
203 self.merge_mock.reset_mock()
203 self.merge_mock.reset_mock()
204 status, msg = PullRequestModel().merge_status(pull_request)
204 status, msg = PullRequestModel().merge_status(pull_request)
205 assert status is False
205 assert status is False
206 assert (
206 assert (
207 msg.eval() ==
207 msg.eval() ==
208 'This pull request cannot be merged because of merge conflicts.')
208 'This pull request cannot be merged because of merge conflicts.')
209 assert self.merge_mock.called is False
209 assert self.merge_mock.called is False
210
210
211 def test_merge_status_unknown_failure(self, pull_request):
211 def test_merge_status_unknown_failure(self, pull_request):
212 self.merge_mock.return_value = MergeResponse(
212 self.merge_mock.return_value = MergeResponse(
213 False, False, None, MergeFailureReason.UNKNOWN)
213 False, False, None, MergeFailureReason.UNKNOWN)
214
214
215 assert pull_request._last_merge_source_rev is None
215 assert pull_request._last_merge_source_rev is None
216 assert pull_request._last_merge_target_rev is None
216 assert pull_request._last_merge_target_rev is None
217 assert pull_request._last_merge_status is None
217 assert pull_request.last_merge_status is None
218
218
219 status, msg = PullRequestModel().merge_status(pull_request)
219 status, msg = PullRequestModel().merge_status(pull_request)
220 assert status is False
220 assert status is False
221 assert msg.eval() == (
221 assert msg.eval() == (
222 'This pull request cannot be merged because of an unhandled'
222 'This pull request cannot be merged because of an unhandled'
223 ' exception.')
223 ' exception.')
224 self.merge_mock.assert_called_once_with(
224 self.merge_mock.assert_called_once_with(
225 pull_request.target_ref_parts,
225 pull_request.target_ref_parts,
226 pull_request.source_repo.scm_instance(),
226 pull_request.source_repo.scm_instance(),
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
228 use_rebase=False)
228 use_rebase=False)
229
229
230 assert pull_request._last_merge_source_rev is None
230 assert pull_request._last_merge_source_rev is None
231 assert pull_request._last_merge_target_rev is None
231 assert pull_request._last_merge_target_rev is None
232 assert pull_request._last_merge_status is None
232 assert pull_request.last_merge_status is None
233
233
234 self.merge_mock.reset_mock()
234 self.merge_mock.reset_mock()
235 status, msg = PullRequestModel().merge_status(pull_request)
235 status, msg = PullRequestModel().merge_status(pull_request)
236 assert status is False
236 assert status is False
237 assert msg.eval() == (
237 assert msg.eval() == (
238 'This pull request cannot be merged because of an unhandled'
238 'This pull request cannot be merged because of an unhandled'
239 ' exception.')
239 ' exception.')
240 assert self.merge_mock.called is True
240 assert self.merge_mock.called is True
241
241
242 def test_merge_status_when_target_is_locked(self, pull_request):
242 def test_merge_status_when_target_is_locked(self, pull_request):
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
244 status, msg = PullRequestModel().merge_status(pull_request)
244 status, msg = PullRequestModel().merge_status(pull_request)
245 assert status is False
245 assert status is False
246 assert msg.eval() == (
246 assert msg.eval() == (
247 'This pull request cannot be merged because the target repository'
247 'This pull request cannot be merged because the target repository'
248 ' is locked.')
248 ' is locked.')
249
249
250 def test_merge_status_requirements_check_target(self, pull_request):
250 def test_merge_status_requirements_check_target(self, pull_request):
251
251
252 def has_largefiles(self, repo):
252 def has_largefiles(self, repo):
253 return repo == pull_request.source_repo
253 return repo == pull_request.source_repo
254
254
255 patcher = mock.patch.object(
255 patcher = mock.patch.object(
256 PullRequestModel, '_has_largefiles', has_largefiles)
256 PullRequestModel, '_has_largefiles', has_largefiles)
257 with patcher:
257 with patcher:
258 status, msg = PullRequestModel().merge_status(pull_request)
258 status, msg = PullRequestModel().merge_status(pull_request)
259
259
260 assert status is False
260 assert status is False
261 assert msg == 'Target repository large files support is disabled.'
261 assert msg == 'Target repository large files support is disabled.'
262
262
263 def test_merge_status_requirements_check_source(self, pull_request):
263 def test_merge_status_requirements_check_source(self, pull_request):
264
264
265 def has_largefiles(self, repo):
265 def has_largefiles(self, repo):
266 return repo == pull_request.target_repo
266 return repo == pull_request.target_repo
267
267
268 patcher = mock.patch.object(
268 patcher = mock.patch.object(
269 PullRequestModel, '_has_largefiles', has_largefiles)
269 PullRequestModel, '_has_largefiles', has_largefiles)
270 with patcher:
270 with patcher:
271 status, msg = PullRequestModel().merge_status(pull_request)
271 status, msg = PullRequestModel().merge_status(pull_request)
272
272
273 assert status is False
273 assert status is False
274 assert msg == 'Source repository large files support is disabled.'
274 assert msg == 'Source repository large files support is disabled.'
275
275
276 def test_merge(self, pull_request, merge_extras):
276 def test_merge(self, pull_request, merge_extras):
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
278 merge_ref = Reference(
278 merge_ref = Reference(
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
280 self.merge_mock.return_value = MergeResponse(
280 self.merge_mock.return_value = MergeResponse(
281 True, True, merge_ref, MergeFailureReason.NONE)
281 True, True, merge_ref, MergeFailureReason.NONE)
282
282
283 merge_extras['repository'] = pull_request.target_repo.repo_name
283 merge_extras['repository'] = pull_request.target_repo.repo_name
284 PullRequestModel().merge(
284 PullRequestModel().merge(
285 pull_request, pull_request.author, extras=merge_extras)
285 pull_request, pull_request.author, extras=merge_extras)
286
286
287 message = (
287 message = (
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
289 u'\n\n {pr_title}'.format(
289 u'\n\n {pr_title}'.format(
290 pr_id=pull_request.pull_request_id,
290 pr_id=pull_request.pull_request_id,
291 source_repo=safe_unicode(
291 source_repo=safe_unicode(
292 pull_request.source_repo.scm_instance().name),
292 pull_request.source_repo.scm_instance().name),
293 source_ref_name=pull_request.source_ref_parts.name,
293 source_ref_name=pull_request.source_ref_parts.name,
294 pr_title=safe_unicode(pull_request.title)
294 pr_title=safe_unicode(pull_request.title)
295 )
295 )
296 )
296 )
297 self.merge_mock.assert_called_once_with(
297 self.merge_mock.assert_called_once_with(
298 pull_request.target_ref_parts,
298 pull_request.target_ref_parts,
299 pull_request.source_repo.scm_instance(),
299 pull_request.source_repo.scm_instance(),
300 pull_request.source_ref_parts, self.workspace_id,
300 pull_request.source_ref_parts, self.workspace_id,
301 user_name=user.username, user_email=user.email, message=message,
301 user_name=user.username, user_email=user.email, message=message,
302 use_rebase=False
302 use_rebase=False
303 )
303 )
304 self.invalidation_mock.assert_called_once_with(
304 self.invalidation_mock.assert_called_once_with(
305 pull_request.target_repo.repo_name)
305 pull_request.target_repo.repo_name)
306
306
307 self.hook_mock.assert_called_with(
307 self.hook_mock.assert_called_with(
308 self.pull_request, self.pull_request.author, 'merge')
308 self.pull_request, self.pull_request.author, 'merge')
309
309
310 pull_request = PullRequest.get(pull_request.pull_request_id)
310 pull_request = PullRequest.get(pull_request.pull_request_id)
311 assert (
311 assert (
312 pull_request.merge_rev ==
312 pull_request.merge_rev ==
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
314
314
315 def test_merge_failed(self, pull_request, merge_extras):
315 def test_merge_failed(self, pull_request, merge_extras):
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
317 merge_ref = Reference(
317 merge_ref = Reference(
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
319 self.merge_mock.return_value = MergeResponse(
319 self.merge_mock.return_value = MergeResponse(
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
321
321
322 merge_extras['repository'] = pull_request.target_repo.repo_name
322 merge_extras['repository'] = pull_request.target_repo.repo_name
323 PullRequestModel().merge(
323 PullRequestModel().merge(
324 pull_request, pull_request.author, extras=merge_extras)
324 pull_request, pull_request.author, extras=merge_extras)
325
325
326 message = (
326 message = (
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
328 u'\n\n {pr_title}'.format(
328 u'\n\n {pr_title}'.format(
329 pr_id=pull_request.pull_request_id,
329 pr_id=pull_request.pull_request_id,
330 source_repo=safe_unicode(
330 source_repo=safe_unicode(
331 pull_request.source_repo.scm_instance().name),
331 pull_request.source_repo.scm_instance().name),
332 source_ref_name=pull_request.source_ref_parts.name,
332 source_ref_name=pull_request.source_ref_parts.name,
333 pr_title=safe_unicode(pull_request.title)
333 pr_title=safe_unicode(pull_request.title)
334 )
334 )
335 )
335 )
336 self.merge_mock.assert_called_once_with(
336 self.merge_mock.assert_called_once_with(
337 pull_request.target_ref_parts,
337 pull_request.target_ref_parts,
338 pull_request.source_repo.scm_instance(),
338 pull_request.source_repo.scm_instance(),
339 pull_request.source_ref_parts, self.workspace_id,
339 pull_request.source_ref_parts, self.workspace_id,
340 user_name=user.username, user_email=user.email, message=message,
340 user_name=user.username, user_email=user.email, message=message,
341 use_rebase=False
341 use_rebase=False
342 )
342 )
343
343
344 pull_request = PullRequest.get(pull_request.pull_request_id)
344 pull_request = PullRequest.get(pull_request.pull_request_id)
345 assert self.invalidation_mock.called is False
345 assert self.invalidation_mock.called is False
346 assert pull_request.merge_rev is None
346 assert pull_request.merge_rev is None
347
347
348 def test_get_commit_ids(self, pull_request):
348 def test_get_commit_ids(self, pull_request):
349 # The PR has been not merget yet, so expect an exception
349 # The PR has been not merget yet, so expect an exception
350 with pytest.raises(ValueError):
350 with pytest.raises(ValueError):
351 PullRequestModel()._get_commit_ids(pull_request)
351 PullRequestModel()._get_commit_ids(pull_request)
352
352
353 # Merge revision is in the revisions list
353 # Merge revision is in the revisions list
354 pull_request.merge_rev = pull_request.revisions[0]
354 pull_request.merge_rev = pull_request.revisions[0]
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
356 assert commit_ids == pull_request.revisions
356 assert commit_ids == pull_request.revisions
357
357
358 # Merge revision is not in the revisions list
358 # Merge revision is not in the revisions list
359 pull_request.merge_rev = 'f000' * 10
359 pull_request.merge_rev = 'f000' * 10
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
362
362
363 def test_get_diff_from_pr_version(self, pull_request):
363 def test_get_diff_from_pr_version(self, pull_request):
364 source_repo = pull_request.source_repo
364 source_repo = pull_request.source_repo
365 source_ref_id = pull_request.source_ref_parts.commit_id
365 source_ref_id = pull_request.source_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
368 source_repo, source_ref_id, target_ref_id, context=6)
368 source_repo, source_ref_id, target_ref_id, context=6)
369 assert 'file_1' in diff.raw
369 assert 'file_1' in diff.raw
370
370
371 def test_generate_title_returns_unicode(self):
371 def test_generate_title_returns_unicode(self):
372 title = PullRequestModel().generate_pullrequest_title(
372 title = PullRequestModel().generate_pullrequest_title(
373 source='source-dummy',
373 source='source-dummy',
374 source_ref='source-ref-dummy',
374 source_ref='source-ref-dummy',
375 target='target-dummy',
375 target='target-dummy',
376 )
376 )
377 assert type(title) == unicode
377 assert type(title) == unicode
378
378
379
379
380 @pytest.mark.usefixtures('config_stub')
380 @pytest.mark.usefixtures('config_stub')
381 class TestIntegrationMerge(object):
381 class TestIntegrationMerge(object):
382 @pytest.mark.parametrize('extra_config', (
382 @pytest.mark.parametrize('extra_config', (
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
384 ))
384 ))
385 def test_merge_triggers_push_hooks(
385 def test_merge_triggers_push_hooks(
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
387 extra_config):
387 extra_config):
388 pull_request = pr_util.create_pull_request(
388 pull_request = pr_util.create_pull_request(
389 approved=True, mergeable=True)
389 approved=True, mergeable=True)
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
391 merge_extras['repository'] = pull_request.target_repo.repo_name
391 merge_extras['repository'] = pull_request.target_repo.repo_name
392 Session().commit()
392 Session().commit()
393
393
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
395 merge_state = PullRequestModel().merge(
395 merge_state = PullRequestModel().merge(
396 pull_request, user_admin, extras=merge_extras)
396 pull_request, user_admin, extras=merge_extras)
397
397
398 assert merge_state.executed
398 assert merge_state.executed
399 assert 'pre_push' in capture_rcextensions
399 assert 'pre_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
401
401
402 def test_merge_can_be_rejected_by_pre_push_hook(
402 def test_merge_can_be_rejected_by_pre_push_hook(
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
404 pull_request = pr_util.create_pull_request(
404 pull_request = pr_util.create_pull_request(
405 approved=True, mergeable=True)
405 approved=True, mergeable=True)
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
407 merge_extras['repository'] = pull_request.target_repo.repo_name
407 merge_extras['repository'] = pull_request.target_repo.repo_name
408 Session().commit()
408 Session().commit()
409
409
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
411 pre_pull.side_effect = RepositoryError("Disallow push!")
411 pre_pull.side_effect = RepositoryError("Disallow push!")
412 merge_status = PullRequestModel().merge(
412 merge_status = PullRequestModel().merge(
413 pull_request, user_admin, extras=merge_extras)
413 pull_request, user_admin, extras=merge_extras)
414
414
415 assert not merge_status.executed
415 assert not merge_status.executed
416 assert 'pre_push' not in capture_rcextensions
416 assert 'pre_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
418
418
419 def test_merge_fails_if_target_is_locked(
419 def test_merge_fails_if_target_is_locked(
420 self, pr_util, user_regular, merge_extras):
420 self, pr_util, user_regular, merge_extras):
421 pull_request = pr_util.create_pull_request(
421 pull_request = pr_util.create_pull_request(
422 approved=True, mergeable=True)
422 approved=True, mergeable=True)
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
424 pull_request.target_repo.locked = locked_by
424 pull_request.target_repo.locked = locked_by
425 # TODO: johbo: Check if this can work based on the database, currently
425 # TODO: johbo: Check if this can work based on the database, currently
426 # all data is pre-computed, that's why just updating the DB is not
426 # all data is pre-computed, that's why just updating the DB is not
427 # enough.
427 # enough.
428 merge_extras['locked_by'] = locked_by
428 merge_extras['locked_by'] = locked_by
429 merge_extras['repository'] = pull_request.target_repo.repo_name
429 merge_extras['repository'] = pull_request.target_repo.repo_name
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
431 Session().commit()
431 Session().commit()
432 merge_status = PullRequestModel().merge(
432 merge_status = PullRequestModel().merge(
433 pull_request, user_regular, extras=merge_extras)
433 pull_request, user_regular, extras=merge_extras)
434 assert not merge_status.executed
434 assert not merge_status.executed
435
435
436
436
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
438 (False, 1, 0),
438 (False, 1, 0),
439 (True, 0, 1),
439 (True, 0, 1),
440 ])
440 ])
441 def test_outdated_comments(
441 def test_outdated_comments(
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
443 pull_request = pr_util.create_pull_request()
443 pull_request = pr_util.create_pull_request()
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
445
445
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
447 pr_util.add_one_commit()
447 pr_util.add_one_commit()
448 assert_inline_comments(
448 assert_inline_comments(
449 pull_request, visible=inlines_count, outdated=outdated_count)
449 pull_request, visible=inlines_count, outdated=outdated_count)
450 outdated_comment_mock.assert_called_with(pull_request)
450 outdated_comment_mock.assert_called_with(pull_request)
451
451
452
452
453 @pytest.fixture
453 @pytest.fixture
454 def merge_extras(user_regular):
454 def merge_extras(user_regular):
455 """
455 """
456 Context for the vcs operation when running a merge.
456 Context for the vcs operation when running a merge.
457 """
457 """
458 extras = {
458 extras = {
459 'ip': '127.0.0.1',
459 'ip': '127.0.0.1',
460 'username': user_regular.username,
460 'username': user_regular.username,
461 'action': 'push',
461 'action': 'push',
462 'repository': 'fake_target_repo_name',
462 'repository': 'fake_target_repo_name',
463 'scm': 'git',
463 'scm': 'git',
464 'config': 'fake_config_ini_path',
464 'config': 'fake_config_ini_path',
465 'make_lock': None,
465 'make_lock': None,
466 'locked_by': [None, None, None],
466 'locked_by': [None, None, None],
467 'server_url': 'http://test.example.com:5000',
467 'server_url': 'http://test.example.com:5000',
468 'hooks': ['push', 'pull'],
468 'hooks': ['push', 'pull'],
469 'is_shadow_repo': False,
469 'is_shadow_repo': False,
470 }
470 }
471 return extras
471 return extras
472
472
473
473
474 @pytest.mark.usefixtures('config_stub')
474 @pytest.mark.usefixtures('config_stub')
475 class TestUpdateCommentHandling(object):
475 class TestUpdateCommentHandling(object):
476
476
477 @pytest.fixture(autouse=True, scope='class')
477 @pytest.fixture(autouse=True, scope='class')
478 def enable_outdated_comments(self, request, pylonsapp):
478 def enable_outdated_comments(self, request, pylonsapp):
479 config_patch = mock.patch.dict(
479 config_patch = mock.patch.dict(
480 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
480 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
481 config_patch.start()
481 config_patch.start()
482
482
483 @request.addfinalizer
483 @request.addfinalizer
484 def cleanup():
484 def cleanup():
485 config_patch.stop()
485 config_patch.stop()
486
486
487 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
487 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
488 commits = [
488 commits = [
489 {'message': 'a'},
489 {'message': 'a'},
490 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
490 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
491 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
491 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
492 ]
492 ]
493 pull_request = pr_util.create_pull_request(
493 pull_request = pr_util.create_pull_request(
494 commits=commits, target_head='a', source_head='b', revisions=['b'])
494 commits=commits, target_head='a', source_head='b', revisions=['b'])
495 pr_util.create_inline_comment(file_path='file_b')
495 pr_util.create_inline_comment(file_path='file_b')
496 pr_util.add_one_commit(head='c')
496 pr_util.add_one_commit(head='c')
497
497
498 assert_inline_comments(pull_request, visible=1, outdated=0)
498 assert_inline_comments(pull_request, visible=1, outdated=0)
499
499
500 def test_comment_stays_unflagged_on_change_above(self, pr_util):
500 def test_comment_stays_unflagged_on_change_above(self, pr_util):
501 original_content = ''.join(
501 original_content = ''.join(
502 ['line {}\n'.format(x) for x in range(1, 11)])
502 ['line {}\n'.format(x) for x in range(1, 11)])
503 updated_content = 'new_line_at_top\n' + original_content
503 updated_content = 'new_line_at_top\n' + original_content
504 commits = [
504 commits = [
505 {'message': 'a'},
505 {'message': 'a'},
506 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
506 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
507 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
507 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
508 ]
508 ]
509 pull_request = pr_util.create_pull_request(
509 pull_request = pr_util.create_pull_request(
510 commits=commits, target_head='a', source_head='b', revisions=['b'])
510 commits=commits, target_head='a', source_head='b', revisions=['b'])
511
511
512 with outdated_comments_patcher():
512 with outdated_comments_patcher():
513 comment = pr_util.create_inline_comment(
513 comment = pr_util.create_inline_comment(
514 line_no=u'n8', file_path='file_b')
514 line_no=u'n8', file_path='file_b')
515 pr_util.add_one_commit(head='c')
515 pr_util.add_one_commit(head='c')
516
516
517 assert_inline_comments(pull_request, visible=1, outdated=0)
517 assert_inline_comments(pull_request, visible=1, outdated=0)
518 assert comment.line_no == u'n9'
518 assert comment.line_no == u'n9'
519
519
520 def test_comment_stays_unflagged_on_change_below(self, pr_util):
520 def test_comment_stays_unflagged_on_change_below(self, pr_util):
521 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
521 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
522 updated_content = original_content + 'new_line_at_end\n'
522 updated_content = original_content + 'new_line_at_end\n'
523 commits = [
523 commits = [
524 {'message': 'a'},
524 {'message': 'a'},
525 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
525 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
526 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
526 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
527 ]
527 ]
528 pull_request = pr_util.create_pull_request(
528 pull_request = pr_util.create_pull_request(
529 commits=commits, target_head='a', source_head='b', revisions=['b'])
529 commits=commits, target_head='a', source_head='b', revisions=['b'])
530 pr_util.create_inline_comment(file_path='file_b')
530 pr_util.create_inline_comment(file_path='file_b')
531 pr_util.add_one_commit(head='c')
531 pr_util.add_one_commit(head='c')
532
532
533 assert_inline_comments(pull_request, visible=1, outdated=0)
533 assert_inline_comments(pull_request, visible=1, outdated=0)
534
534
535 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
535 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
536 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
536 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
537 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
537 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
538 change_lines = list(base_lines)
538 change_lines = list(base_lines)
539 change_lines.insert(6, 'line 6a added\n')
539 change_lines.insert(6, 'line 6a added\n')
540
540
541 # Changes on the last line of sight
541 # Changes on the last line of sight
542 update_lines = list(change_lines)
542 update_lines = list(change_lines)
543 update_lines[0] = 'line 1 changed\n'
543 update_lines[0] = 'line 1 changed\n'
544 update_lines[-1] = 'line 12 changed\n'
544 update_lines[-1] = 'line 12 changed\n'
545
545
546 def file_b(lines):
546 def file_b(lines):
547 return FileNode('file_b', ''.join(lines))
547 return FileNode('file_b', ''.join(lines))
548
548
549 commits = [
549 commits = [
550 {'message': 'a', 'added': [file_b(base_lines)]},
550 {'message': 'a', 'added': [file_b(base_lines)]},
551 {'message': 'b', 'changed': [file_b(change_lines)]},
551 {'message': 'b', 'changed': [file_b(change_lines)]},
552 {'message': 'c', 'changed': [file_b(update_lines)]},
552 {'message': 'c', 'changed': [file_b(update_lines)]},
553 ]
553 ]
554
554
555 pull_request = pr_util.create_pull_request(
555 pull_request = pr_util.create_pull_request(
556 commits=commits, target_head='a', source_head='b', revisions=['b'])
556 commits=commits, target_head='a', source_head='b', revisions=['b'])
557 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
557 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
558
558
559 with outdated_comments_patcher():
559 with outdated_comments_patcher():
560 pr_util.add_one_commit(head='c')
560 pr_util.add_one_commit(head='c')
561 assert_inline_comments(pull_request, visible=0, outdated=1)
561 assert_inline_comments(pull_request, visible=0, outdated=1)
562
562
563 @pytest.mark.parametrize("change, content", [
563 @pytest.mark.parametrize("change, content", [
564 ('changed', 'changed\n'),
564 ('changed', 'changed\n'),
565 ('removed', ''),
565 ('removed', ''),
566 ], ids=['changed', 'removed'])
566 ], ids=['changed', 'removed'])
567 def test_comment_flagged_on_change(self, pr_util, change, content):
567 def test_comment_flagged_on_change(self, pr_util, change, content):
568 commits = [
568 commits = [
569 {'message': 'a'},
569 {'message': 'a'},
570 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
570 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
571 {'message': 'c', change: [FileNode('file_b', content)]},
571 {'message': 'c', change: [FileNode('file_b', content)]},
572 ]
572 ]
573 pull_request = pr_util.create_pull_request(
573 pull_request = pr_util.create_pull_request(
574 commits=commits, target_head='a', source_head='b', revisions=['b'])
574 commits=commits, target_head='a', source_head='b', revisions=['b'])
575 pr_util.create_inline_comment(file_path='file_b')
575 pr_util.create_inline_comment(file_path='file_b')
576
576
577 with outdated_comments_patcher():
577 with outdated_comments_patcher():
578 pr_util.add_one_commit(head='c')
578 pr_util.add_one_commit(head='c')
579 assert_inline_comments(pull_request, visible=0, outdated=1)
579 assert_inline_comments(pull_request, visible=0, outdated=1)
580
580
581
581
582 @pytest.mark.usefixtures('config_stub')
582 @pytest.mark.usefixtures('config_stub')
583 class TestUpdateChangedFiles(object):
583 class TestUpdateChangedFiles(object):
584
584
585 def test_no_changes_on_unchanged_diff(self, pr_util):
585 def test_no_changes_on_unchanged_diff(self, pr_util):
586 commits = [
586 commits = [
587 {'message': 'a'},
587 {'message': 'a'},
588 {'message': 'b',
588 {'message': 'b',
589 'added': [FileNode('file_b', 'test_content b\n')]},
589 'added': [FileNode('file_b', 'test_content b\n')]},
590 {'message': 'c',
590 {'message': 'c',
591 'added': [FileNode('file_c', 'test_content c\n')]},
591 'added': [FileNode('file_c', 'test_content c\n')]},
592 ]
592 ]
593 # open a PR from a to b, adding file_b
593 # open a PR from a to b, adding file_b
594 pull_request = pr_util.create_pull_request(
594 pull_request = pr_util.create_pull_request(
595 commits=commits, target_head='a', source_head='b', revisions=['b'],
595 commits=commits, target_head='a', source_head='b', revisions=['b'],
596 name_suffix='per-file-review')
596 name_suffix='per-file-review')
597
597
598 # modify PR adding new file file_c
598 # modify PR adding new file file_c
599 pr_util.add_one_commit(head='c')
599 pr_util.add_one_commit(head='c')
600
600
601 assert_pr_file_changes(
601 assert_pr_file_changes(
602 pull_request,
602 pull_request,
603 added=['file_c'],
603 added=['file_c'],
604 modified=[],
604 modified=[],
605 removed=[])
605 removed=[])
606
606
607 def test_modify_and_undo_modification_diff(self, pr_util):
607 def test_modify_and_undo_modification_diff(self, pr_util):
608 commits = [
608 commits = [
609 {'message': 'a'},
609 {'message': 'a'},
610 {'message': 'b',
610 {'message': 'b',
611 'added': [FileNode('file_b', 'test_content b\n')]},
611 'added': [FileNode('file_b', 'test_content b\n')]},
612 {'message': 'c',
612 {'message': 'c',
613 'changed': [FileNode('file_b', 'test_content b modified\n')]},
613 'changed': [FileNode('file_b', 'test_content b modified\n')]},
614 {'message': 'd',
614 {'message': 'd',
615 'changed': [FileNode('file_b', 'test_content b\n')]},
615 'changed': [FileNode('file_b', 'test_content b\n')]},
616 ]
616 ]
617 # open a PR from a to b, adding file_b
617 # open a PR from a to b, adding file_b
618 pull_request = pr_util.create_pull_request(
618 pull_request = pr_util.create_pull_request(
619 commits=commits, target_head='a', source_head='b', revisions=['b'],
619 commits=commits, target_head='a', source_head='b', revisions=['b'],
620 name_suffix='per-file-review')
620 name_suffix='per-file-review')
621
621
622 # modify PR modifying file file_b
622 # modify PR modifying file file_b
623 pr_util.add_one_commit(head='c')
623 pr_util.add_one_commit(head='c')
624
624
625 assert_pr_file_changes(
625 assert_pr_file_changes(
626 pull_request,
626 pull_request,
627 added=[],
627 added=[],
628 modified=['file_b'],
628 modified=['file_b'],
629 removed=[])
629 removed=[])
630
630
631 # move the head again to d, which rollbacks change,
631 # move the head again to d, which rollbacks change,
632 # meaning we should indicate no changes
632 # meaning we should indicate no changes
633 pr_util.add_one_commit(head='d')
633 pr_util.add_one_commit(head='d')
634
634
635 assert_pr_file_changes(
635 assert_pr_file_changes(
636 pull_request,
636 pull_request,
637 added=[],
637 added=[],
638 modified=[],
638 modified=[],
639 removed=[])
639 removed=[])
640
640
641 def test_updated_all_files_in_pr(self, pr_util):
641 def test_updated_all_files_in_pr(self, pr_util):
642 commits = [
642 commits = [
643 {'message': 'a'},
643 {'message': 'a'},
644 {'message': 'b', 'added': [
644 {'message': 'b', 'added': [
645 FileNode('file_a', 'test_content a\n'),
645 FileNode('file_a', 'test_content a\n'),
646 FileNode('file_b', 'test_content b\n'),
646 FileNode('file_b', 'test_content b\n'),
647 FileNode('file_c', 'test_content c\n')]},
647 FileNode('file_c', 'test_content c\n')]},
648 {'message': 'c', 'changed': [
648 {'message': 'c', 'changed': [
649 FileNode('file_a', 'test_content a changed\n'),
649 FileNode('file_a', 'test_content a changed\n'),
650 FileNode('file_b', 'test_content b changed\n'),
650 FileNode('file_b', 'test_content b changed\n'),
651 FileNode('file_c', 'test_content c changed\n')]},
651 FileNode('file_c', 'test_content c changed\n')]},
652 ]
652 ]
653 # open a PR from a to b, changing 3 files
653 # open a PR from a to b, changing 3 files
654 pull_request = pr_util.create_pull_request(
654 pull_request = pr_util.create_pull_request(
655 commits=commits, target_head='a', source_head='b', revisions=['b'],
655 commits=commits, target_head='a', source_head='b', revisions=['b'],
656 name_suffix='per-file-review')
656 name_suffix='per-file-review')
657
657
658 pr_util.add_one_commit(head='c')
658 pr_util.add_one_commit(head='c')
659
659
660 assert_pr_file_changes(
660 assert_pr_file_changes(
661 pull_request,
661 pull_request,
662 added=[],
662 added=[],
663 modified=['file_a', 'file_b', 'file_c'],
663 modified=['file_a', 'file_b', 'file_c'],
664 removed=[])
664 removed=[])
665
665
666 def test_updated_and_removed_all_files_in_pr(self, pr_util):
666 def test_updated_and_removed_all_files_in_pr(self, pr_util):
667 commits = [
667 commits = [
668 {'message': 'a'},
668 {'message': 'a'},
669 {'message': 'b', 'added': [
669 {'message': 'b', 'added': [
670 FileNode('file_a', 'test_content a\n'),
670 FileNode('file_a', 'test_content a\n'),
671 FileNode('file_b', 'test_content b\n'),
671 FileNode('file_b', 'test_content b\n'),
672 FileNode('file_c', 'test_content c\n')]},
672 FileNode('file_c', 'test_content c\n')]},
673 {'message': 'c', 'removed': [
673 {'message': 'c', 'removed': [
674 FileNode('file_a', 'test_content a changed\n'),
674 FileNode('file_a', 'test_content a changed\n'),
675 FileNode('file_b', 'test_content b changed\n'),
675 FileNode('file_b', 'test_content b changed\n'),
676 FileNode('file_c', 'test_content c changed\n')]},
676 FileNode('file_c', 'test_content c changed\n')]},
677 ]
677 ]
678 # open a PR from a to b, removing 3 files
678 # open a PR from a to b, removing 3 files
679 pull_request = pr_util.create_pull_request(
679 pull_request = pr_util.create_pull_request(
680 commits=commits, target_head='a', source_head='b', revisions=['b'],
680 commits=commits, target_head='a', source_head='b', revisions=['b'],
681 name_suffix='per-file-review')
681 name_suffix='per-file-review')
682
682
683 pr_util.add_one_commit(head='c')
683 pr_util.add_one_commit(head='c')
684
684
685 assert_pr_file_changes(
685 assert_pr_file_changes(
686 pull_request,
686 pull_request,
687 added=[],
687 added=[],
688 modified=[],
688 modified=[],
689 removed=['file_a', 'file_b', 'file_c'])
689 removed=['file_a', 'file_b', 'file_c'])
690
690
691
691
692 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
692 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
693 model = PullRequestModel()
693 model = PullRequestModel()
694 pull_request = pr_util.create_pull_request()
694 pull_request = pr_util.create_pull_request()
695 pr_util.update_source_repository()
695 pr_util.update_source_repository()
696
696
697 model.update_commits(pull_request)
697 model.update_commits(pull_request)
698
698
699 # Expect that it has a version entry now
699 # Expect that it has a version entry now
700 assert len(model.get_versions(pull_request)) == 1
700 assert len(model.get_versions(pull_request)) == 1
701
701
702
702
703 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
703 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
704 pull_request = pr_util.create_pull_request()
704 pull_request = pr_util.create_pull_request()
705 model = PullRequestModel()
705 model = PullRequestModel()
706 model.update_commits(pull_request)
706 model.update_commits(pull_request)
707
707
708 # Expect that it still has no versions
708 # Expect that it still has no versions
709 assert len(model.get_versions(pull_request)) == 0
709 assert len(model.get_versions(pull_request)) == 0
710
710
711
711
712 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
712 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
713 model = PullRequestModel()
713 model = PullRequestModel()
714 pull_request = pr_util.create_pull_request()
714 pull_request = pr_util.create_pull_request()
715 comment = pr_util.create_comment()
715 comment = pr_util.create_comment()
716 pr_util.update_source_repository()
716 pr_util.update_source_repository()
717
717
718 model.update_commits(pull_request)
718 model.update_commits(pull_request)
719
719
720 # Expect that the comment is linked to the pr version now
720 # Expect that the comment is linked to the pr version now
721 assert comment.pull_request_version == model.get_versions(pull_request)[0]
721 assert comment.pull_request_version == model.get_versions(pull_request)[0]
722
722
723
723
724 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
724 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
725 model = PullRequestModel()
725 model = PullRequestModel()
726 pull_request = pr_util.create_pull_request()
726 pull_request = pr_util.create_pull_request()
727 pr_util.update_source_repository()
727 pr_util.update_source_repository()
728 pr_util.update_source_repository()
728 pr_util.update_source_repository()
729
729
730 model.update_commits(pull_request)
730 model.update_commits(pull_request)
731
731
732 # Expect to find a new comment about the change
732 # Expect to find a new comment about the change
733 expected_message = textwrap.dedent(
733 expected_message = textwrap.dedent(
734 """\
734 """\
735 Pull request updated. Auto status change to |under_review|
735 Pull request updated. Auto status change to |under_review|
736
736
737 .. role:: added
737 .. role:: added
738 .. role:: removed
738 .. role:: removed
739 .. parsed-literal::
739 .. parsed-literal::
740
740
741 Changed commits:
741 Changed commits:
742 * :added:`1 added`
742 * :added:`1 added`
743 * :removed:`0 removed`
743 * :removed:`0 removed`
744
744
745 Changed files:
745 Changed files:
746 * `A file_2 <#a_c--92ed3b5f07b4>`_
746 * `A file_2 <#a_c--92ed3b5f07b4>`_
747
747
748 .. |under_review| replace:: *"Under Review"*"""
748 .. |under_review| replace:: *"Under Review"*"""
749 )
749 )
750 pull_request_comments = sorted(
750 pull_request_comments = sorted(
751 pull_request.comments, key=lambda c: c.modified_at)
751 pull_request.comments, key=lambda c: c.modified_at)
752 update_comment = pull_request_comments[-1]
752 update_comment = pull_request_comments[-1]
753 assert update_comment.text == expected_message
753 assert update_comment.text == expected_message
754
754
755
755
756 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
756 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
757 pull_request = pr_util.create_pull_request()
757 pull_request = pr_util.create_pull_request()
758
758
759 # Avoiding default values
759 # Avoiding default values
760 pull_request.status = PullRequest.STATUS_CLOSED
760 pull_request.status = PullRequest.STATUS_CLOSED
761 pull_request._last_merge_source_rev = "0" * 40
761 pull_request._last_merge_source_rev = "0" * 40
762 pull_request._last_merge_target_rev = "1" * 40
762 pull_request._last_merge_target_rev = "1" * 40
763 pull_request._last_merge_status = 1
763 pull_request.last_merge_status = 1
764 pull_request.merge_rev = "2" * 40
764 pull_request.merge_rev = "2" * 40
765
765
766 # Remember automatic values
766 # Remember automatic values
767 created_on = pull_request.created_on
767 created_on = pull_request.created_on
768 updated_on = pull_request.updated_on
768 updated_on = pull_request.updated_on
769
769
770 # Create a new version of the pull request
770 # Create a new version of the pull request
771 version = PullRequestModel()._create_version_from_snapshot(pull_request)
771 version = PullRequestModel()._create_version_from_snapshot(pull_request)
772
772
773 # Check attributes
773 # Check attributes
774 assert version.title == pr_util.create_parameters['title']
774 assert version.title == pr_util.create_parameters['title']
775 assert version.description == pr_util.create_parameters['description']
775 assert version.description == pr_util.create_parameters['description']
776 assert version.status == PullRequest.STATUS_CLOSED
776 assert version.status == PullRequest.STATUS_CLOSED
777
777
778 # versions get updated created_on
778 # versions get updated created_on
779 assert version.created_on != created_on
779 assert version.created_on != created_on
780
780
781 assert version.updated_on == updated_on
781 assert version.updated_on == updated_on
782 assert version.user_id == pull_request.user_id
782 assert version.user_id == pull_request.user_id
783 assert version.revisions == pr_util.create_parameters['revisions']
783 assert version.revisions == pr_util.create_parameters['revisions']
784 assert version.source_repo == pr_util.source_repository
784 assert version.source_repo == pr_util.source_repository
785 assert version.source_ref == pr_util.create_parameters['source_ref']
785 assert version.source_ref == pr_util.create_parameters['source_ref']
786 assert version.target_repo == pr_util.target_repository
786 assert version.target_repo == pr_util.target_repository
787 assert version.target_ref == pr_util.create_parameters['target_ref']
787 assert version.target_ref == pr_util.create_parameters['target_ref']
788 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
788 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
789 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
789 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
790 assert version._last_merge_status == pull_request._last_merge_status
790 assert version.last_merge_status == pull_request.last_merge_status
791 assert version.merge_rev == pull_request.merge_rev
791 assert version.merge_rev == pull_request.merge_rev
792 assert version.pull_request == pull_request
792 assert version.pull_request == pull_request
793
793
794
794
795 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
795 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
796 version1 = pr_util.create_version_of_pull_request()
796 version1 = pr_util.create_version_of_pull_request()
797 comment_linked = pr_util.create_comment(linked_to=version1)
797 comment_linked = pr_util.create_comment(linked_to=version1)
798 comment_unlinked = pr_util.create_comment()
798 comment_unlinked = pr_util.create_comment()
799 version2 = pr_util.create_version_of_pull_request()
799 version2 = pr_util.create_version_of_pull_request()
800
800
801 PullRequestModel()._link_comments_to_version(version2)
801 PullRequestModel()._link_comments_to_version(version2)
802
802
803 # Expect that only the new comment is linked to version2
803 # Expect that only the new comment is linked to version2
804 assert (
804 assert (
805 comment_unlinked.pull_request_version_id ==
805 comment_unlinked.pull_request_version_id ==
806 version2.pull_request_version_id)
806 version2.pull_request_version_id)
807 assert (
807 assert (
808 comment_linked.pull_request_version_id ==
808 comment_linked.pull_request_version_id ==
809 version1.pull_request_version_id)
809 version1.pull_request_version_id)
810 assert (
810 assert (
811 comment_unlinked.pull_request_version_id !=
811 comment_unlinked.pull_request_version_id !=
812 comment_linked.pull_request_version_id)
812 comment_linked.pull_request_version_id)
813
813
814
814
815 def test_calculate_commits():
815 def test_calculate_commits():
816 old_ids = [1, 2, 3]
816 old_ids = [1, 2, 3]
817 new_ids = [1, 3, 4, 5]
817 new_ids = [1, 3, 4, 5]
818 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
818 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
819 assert change.added == [4, 5]
819 assert change.added == [4, 5]
820 assert change.common == [1, 3]
820 assert change.common == [1, 3]
821 assert change.removed == [2]
821 assert change.removed == [2]
822 assert change.total == [1, 3, 4, 5]
822 assert change.total == [1, 3, 4, 5]
823
823
824
824
825 def assert_inline_comments(pull_request, visible=None, outdated=None):
825 def assert_inline_comments(pull_request, visible=None, outdated=None):
826 if visible is not None:
826 if visible is not None:
827 inline_comments = CommentsModel().get_inline_comments(
827 inline_comments = CommentsModel().get_inline_comments(
828 pull_request.target_repo.repo_id, pull_request=pull_request)
828 pull_request.target_repo.repo_id, pull_request=pull_request)
829 inline_cnt = CommentsModel().get_inline_comments_count(
829 inline_cnt = CommentsModel().get_inline_comments_count(
830 inline_comments)
830 inline_comments)
831 assert inline_cnt == visible
831 assert inline_cnt == visible
832 if outdated is not None:
832 if outdated is not None:
833 outdated_comments = CommentsModel().get_outdated_comments(
833 outdated_comments = CommentsModel().get_outdated_comments(
834 pull_request.target_repo.repo_id, pull_request)
834 pull_request.target_repo.repo_id, pull_request)
835 assert len(outdated_comments) == outdated
835 assert len(outdated_comments) == outdated
836
836
837
837
838 def assert_pr_file_changes(
838 def assert_pr_file_changes(
839 pull_request, added=None, modified=None, removed=None):
839 pull_request, added=None, modified=None, removed=None):
840 pr_versions = PullRequestModel().get_versions(pull_request)
840 pr_versions = PullRequestModel().get_versions(pull_request)
841 # always use first version, ie original PR to calculate changes
841 # always use first version, ie original PR to calculate changes
842 pull_request_version = pr_versions[0]
842 pull_request_version = pr_versions[0]
843 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
843 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
844 pull_request, pull_request_version)
844 pull_request, pull_request_version)
845 file_changes = PullRequestModel()._calculate_file_changes(
845 file_changes = PullRequestModel()._calculate_file_changes(
846 old_diff_data, new_diff_data)
846 old_diff_data, new_diff_data)
847
847
848 assert added == file_changes.added, \
848 assert added == file_changes.added, \
849 'expected added:%s vs value:%s' % (added, file_changes.added)
849 'expected added:%s vs value:%s' % (added, file_changes.added)
850 assert modified == file_changes.modified, \
850 assert modified == file_changes.modified, \
851 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
851 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
852 assert removed == file_changes.removed, \
852 assert removed == file_changes.removed, \
853 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
853 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
854
854
855
855
856 def outdated_comments_patcher(use_outdated=True):
856 def outdated_comments_patcher(use_outdated=True):
857 return mock.patch.object(
857 return mock.patch.object(
858 CommentsModel, 'use_outdated_comments',
858 CommentsModel, 'use_outdated_comments',
859 return_value=use_outdated)
859 return_value=use_outdated)
General Comments 0
You need to be logged in to leave comments. Login now