##// END OF EJS Templates
models: removed utf8 markers
super-admin -
r5055:a976f41d default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,135 +1,134 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21
20
22 import logging
21 import logging
23
22
24 import rhodecode
23 import rhodecode
25 from rhodecode.model import meta, db
24 from rhodecode.model import meta, db
26 from rhodecode.lib.utils2 import obfuscate_url_pw, get_encryption_key
25 from rhodecode.lib.utils2 import obfuscate_url_pw, get_encryption_key
27
26
28 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
29
28
30
29
31 def init_model(engine, encryption_key=None):
30 def init_model(engine, encryption_key=None):
32 """
31 """
33 Initializes db session, bind the engine with the metadata,
32 Initializes db session, bind the engine with the metadata,
34 Call this before using any of the tables or classes in the model,
33 Call this before using any of the tables or classes in the model,
35 preferably once in application start
34 preferably once in application start
36
35
37 :param engine: engine to bind to
36 :param engine: engine to bind to
38 """
37 """
39 engine_str = obfuscate_url_pw(str(engine.url))
38 engine_str = obfuscate_url_pw(str(engine.url))
40 log.info("RhodeCode %s initializing db for %s", rhodecode.__version__, engine_str)
39 log.info("RhodeCode %s initializing db for %s", rhodecode.__version__, engine_str)
41 meta.Base.metadata.bind = engine
40 meta.Base.metadata.bind = engine
42 db.ENCRYPTION_KEY = encryption_key
41 db.ENCRYPTION_KEY = encryption_key
43
42
44
43
45 def init_model_encryption(migration_models, config=None):
44 def init_model_encryption(migration_models, config=None):
46 from pyramid.threadlocal import get_current_registry
45 from pyramid.threadlocal import get_current_registry
47 config = config or get_current_registry().settings
46 config = config or get_current_registry().settings
48 migration_models.ENCRYPTION_KEY = get_encryption_key(config)
47 migration_models.ENCRYPTION_KEY = get_encryption_key(config)
49 db.ENCRYPTION_KEY = get_encryption_key(config)
48 db.ENCRYPTION_KEY = get_encryption_key(config)
50
49
51
50
52 class BaseModel(object):
51 class BaseModel(object):
53 """
52 """
54 Base Model for all RhodeCode models, it adds sql alchemy session
53 Base Model for all RhodeCode models, it adds sql alchemy session
55 into instance of model
54 into instance of model
56
55
57 :param sa: If passed it reuses this session instead of creating a new one
56 :param sa: If passed it reuses this session instead of creating a new one
58 """
57 """
59
58
60 cls = None # override in child class
59 cls = None # override in child class
61
60
62 def __init__(self, sa=None):
61 def __init__(self, sa=None):
63 if sa is not None:
62 if sa is not None:
64 self.sa = sa
63 self.sa = sa
65 else:
64 else:
66 self.sa = meta.Session()
65 self.sa = meta.Session()
67
66
68 def _get_instance(self, cls, instance, callback=None):
67 def _get_instance(self, cls, instance, callback=None):
69 """
68 """
70 Gets instance of given cls using some simple lookup mechanism.
69 Gets instance of given cls using some simple lookup mechanism.
71
70
72 :param cls: classes to fetch
71 :param cls: classes to fetch
73 :param instance: int or Instance
72 :param instance: int or Instance
74 :param callback: callback to call if all lookups failed
73 :param callback: callback to call if all lookups failed
75 """
74 """
76
75
77 if isinstance(instance, cls):
76 if isinstance(instance, cls):
78 return instance
77 return instance
79 elif isinstance(instance, int):
78 elif isinstance(instance, int):
80 if isinstance(cls, tuple):
79 if isinstance(cls, tuple):
81 # if we pass multi instances we pick first to .get()
80 # if we pass multi instances we pick first to .get()
82 cls = cls[0]
81 cls = cls[0]
83 return cls.get(instance)
82 return cls.get(instance)
84 else:
83 else:
85 if instance:
84 if instance:
86 if callback is None:
85 if callback is None:
87 raise Exception(
86 raise Exception(
88 'given object must be int or Instance of %s '
87 'given object must be int or Instance of %s '
89 'got %s, no callback provided' % (cls, type(instance))
88 'got %s, no callback provided' % (cls, type(instance))
90 )
89 )
91 else:
90 else:
92 return callback(instance)
91 return callback(instance)
93
92
94 def _get_user(self, user):
93 def _get_user(self, user):
95 """
94 """
96 Helper method to get user by ID, or username fallback
95 Helper method to get user by ID, or username fallback
97
96
98 :param user: UserID, username, or User instance
97 :param user: UserID, username, or User instance
99 """
98 """
100 return self._get_instance(
99 return self._get_instance(
101 db.User, user, callback=db.User.get_by_username)
100 db.User, user, callback=db.User.get_by_username)
102
101
103 def _get_user_group(self, user_group):
102 def _get_user_group(self, user_group):
104 """
103 """
105 Helper method to get user by ID, or username fallback
104 Helper method to get user by ID, or username fallback
106
105
107 :param user_group: UserGroupID, user_group_name, or UserGroup instance
106 :param user_group: UserGroupID, user_group_name, or UserGroup instance
108 """
107 """
109 return self._get_instance(
108 return self._get_instance(
110 db.UserGroup, user_group, callback=db.UserGroup.get_by_group_name)
109 db.UserGroup, user_group, callback=db.UserGroup.get_by_group_name)
111
110
112 def _get_repo(self, repository):
111 def _get_repo(self, repository):
113 """
112 """
114 Helper method to get repository by ID, or repository name
113 Helper method to get repository by ID, or repository name
115
114
116 :param repository: RepoID, repository name or Repository Instance
115 :param repository: RepoID, repository name or Repository Instance
117 """
116 """
118 return self._get_instance(
117 return self._get_instance(
119 db.Repository, repository, callback=db.Repository.get_by_repo_name)
118 db.Repository, repository, callback=db.Repository.get_by_repo_name)
120
119
121 def _get_perm(self, permission):
120 def _get_perm(self, permission):
122 """
121 """
123 Helper method to get permission by ID, or permission name
122 Helper method to get permission by ID, or permission name
124
123
125 :param permission: PermissionID, permission_name or Permission instance
124 :param permission: PermissionID, permission_name or Permission instance
126 """
125 """
127 return self._get_instance(
126 return self._get_instance(
128 db.Permission, permission, callback=db.Permission.get_by_key)
127 db.Permission, permission, callback=db.Permission.get_by_key)
129
128
130 @classmethod
129 @classmethod
131 def get_all(cls):
130 def get_all(cls):
132 """
131 """
133 Returns all instances of what is defined in `cls` class variable
132 Returns all instances of what is defined in `cls` class variable
134 """
133 """
135 return cls.cls.getAll()
134 return cls.cls.getAll()
@@ -1,124 +1,124 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2013-2020 RhodeCode GmbH
3 # Copyright (C) 2013-2020 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 authentication tokens model for RhodeCode
22 authentication tokens model for RhodeCode
23 """
23 """
24
24
25 import time
25 import time
26 import logging
26 import logging
27 import traceback
27 import traceback
28 from sqlalchemy import or_
28 from sqlalchemy import or_
29
29
30 from rhodecode.model import BaseModel
30 from rhodecode.model import BaseModel
31 from rhodecode.model.db import UserApiKeys
31 from rhodecode.model.db import UserApiKeys
32 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 class AuthTokenModel(BaseModel):
37 class AuthTokenModel(BaseModel):
38 cls = UserApiKeys
38 cls = UserApiKeys
39
39
40 @classmethod
40 @classmethod
41 def get_lifetime_values(cls, translator):
41 def get_lifetime_values(cls, translator):
42 from rhodecode.lib import helpers as h
42 from rhodecode.lib import helpers as h
43 _ = translator
43 _ = translator
44
44
45 def date_after_min(mins):
45 def date_after_min(mins):
46 after = time.time() + (60 * mins)
46 after = time.time() + (60 * mins)
47 return h.format_date(h.time_to_datetime(after))
47 return h.format_date(h.time_to_datetime(after))
48
48
49 return [
49 return [
50 (str(-1),
50 (str(-1),
51 _('forever')),
51 _('forever')),
52 (str(5),
52 (str(5),
53 _('5 minutes {end_date}').format(end_date=date_after_min(5))),
53 _('5 minutes {end_date}').format(end_date=date_after_min(5))),
54 (str(60),
54 (str(60),
55 _('1 hour {end_date}').format(end_date=date_after_min(60))),
55 _('1 hour {end_date}').format(end_date=date_after_min(60))),
56 (str(60 * 24),
56 (str(60 * 24),
57 _('1 day {end_date}').format(end_date=date_after_min(60 * 24))),
57 _('1 day {end_date}').format(end_date=date_after_min(60 * 24))),
58 (str(60 * 24 * 30),
58 (str(60 * 24 * 30),
59 _('1 month {end_date}').format(end_date=date_after_min(60 * 24 * 30))),
59 _('1 month {end_date}').format(end_date=date_after_min(60 * 24 * 30))),
60 ]
60 ]
61
61
62 def create(self, user, description, lifetime=-1, role=UserApiKeys.ROLE_ALL):
62 def create(self, user, description, lifetime=-1, role=UserApiKeys.ROLE_ALL):
63 """
63 """
64 :param user: user or user_id
64 :param user: user or user_id
65 :param description: description of ApiKey
65 :param description: description of ApiKey
66 :param lifetime: expiration time in minutes
66 :param lifetime: expiration time in minutes
67 :param role: role for the apikey
67 :param role: role for the apikey
68 """
68 """
69 from rhodecode.lib.auth import generate_auth_token
69 from rhodecode.lib.auth import generate_auth_token
70
70
71 user = self._get_user(user)
71 user = self._get_user(user)
72
72
73 new_auth_token = UserApiKeys()
73 new_auth_token = UserApiKeys()
74 new_auth_token.api_key = generate_auth_token(user.username)
74 new_auth_token.api_key = generate_auth_token(user.username)
75 new_auth_token.user_id = user.user_id
75 new_auth_token.user_id = user.user_id
76 new_auth_token.description = description
76 new_auth_token.description = description
77 new_auth_token.role = role
77 new_auth_token.role = role
78 new_auth_token.expires = time.time() + (lifetime * 60) \
78 new_auth_token.expires = time.time() + (lifetime * 60) \
79 if lifetime != -1 else -1
79 if lifetime != -1 else -1
80 Session().add(new_auth_token)
80 Session().add(new_auth_token)
81
81
82 return new_auth_token
82 return new_auth_token
83
83
84 def delete(self, auth_token_id, user=None):
84 def delete(self, auth_token_id, user=None):
85 """
85 """
86 Deletes given api_key, if user is set it also filters the object for
86 Deletes given api_key, if user is set it also filters the object for
87 deletion by given user.
87 deletion by given user.
88 """
88 """
89 auth_token = UserApiKeys.query().filter(
89 auth_token = UserApiKeys.query().filter(
90 UserApiKeys.user_api_key_id == auth_token_id)
90 UserApiKeys.user_api_key_id == auth_token_id)
91
91
92 if user:
92 if user:
93 user = self._get_user(user)
93 user = self._get_user(user)
94 auth_token = auth_token.filter(UserApiKeys.user_id == user.user_id)
94 auth_token = auth_token.filter(UserApiKeys.user_id == user.user_id)
95 auth_token = auth_token.scalar()
95 auth_token = auth_token.scalar()
96
96
97 if auth_token:
97 if auth_token:
98 try:
98 try:
99 Session().delete(auth_token)
99 Session().delete(auth_token)
100 except Exception:
100 except Exception:
101 log.error(traceback.format_exc())
101 log.error(traceback.format_exc())
102 raise
102 raise
103
103
104 def get_auth_tokens(self, user, show_expired=True):
104 def get_auth_tokens(self, user, show_expired=True):
105 user = self._get_user(user)
105 user = self._get_user(user)
106 user_auth_tokens = UserApiKeys.query()\
106 user_auth_tokens = UserApiKeys.query()\
107 .filter(UserApiKeys.user_id == user.user_id)
107 .filter(UserApiKeys.user_id == user.user_id)
108 if not show_expired:
108 if not show_expired:
109 user_auth_tokens = user_auth_tokens\
109 user_auth_tokens = user_auth_tokens\
110 .filter(or_(UserApiKeys.expires == -1,
110 .filter(or_(UserApiKeys.expires == -1,
111 UserApiKeys.expires >= time.time()))
111 UserApiKeys.expires >= time.time()))
112 user_auth_tokens = user_auth_tokens.order_by(
112 user_auth_tokens = user_auth_tokens.order_by(
113 UserApiKeys.user_api_key_id)
113 UserApiKeys.user_api_key_id)
114 return user_auth_tokens
114 return user_auth_tokens
115
115
116 def get_auth_token(self, auth_token):
116 def get_auth_token(self, auth_token):
117 auth_token = UserApiKeys.query().filter(
117 auth_token = UserApiKeys.query().filter(
118 UserApiKeys.api_key == auth_token)
118 UserApiKeys.api_key == auth_token)
119 auth_token = auth_token \
119 auth_token = auth_token \
120 .filter(or_(UserApiKeys.expires == -1,
120 .filter(or_(UserApiKeys.expires == -1,
121 UserApiKeys.expires >= time.time()))\
121 UserApiKeys.expires >= time.time()))\
122 .first()
122 .first()
123
123
124 return auth_token
124 return auth_token
@@ -1,403 +1,402 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21
20
22 import itertools
21 import itertools
23 import logging
22 import logging
24 import collections
23 import collections
25
24
26 from rhodecode.model import BaseModel
25 from rhodecode.model import BaseModel
27 from rhodecode.model.db import (
26 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
27 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
28 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 from rhodecode.lib.markup_renderer import (
29 from rhodecode.lib.markup_renderer import (
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
30 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
32
31
33 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
34
33
35
34
36 class ChangesetStatusModel(BaseModel):
35 class ChangesetStatusModel(BaseModel):
37
36
38 cls = ChangesetStatus
37 cls = ChangesetStatus
39
38
40 def __get_changeset_status(self, changeset_status):
39 def __get_changeset_status(self, changeset_status):
41 return self._get_instance(ChangesetStatus, changeset_status)
40 return self._get_instance(ChangesetStatus, changeset_status)
42
41
43 def __get_pull_request(self, pull_request):
42 def __get_pull_request(self, pull_request):
44 return self._get_instance(PullRequest, pull_request)
43 return self._get_instance(PullRequest, pull_request)
45
44
46 def _get_status_query(self, repo, revision, pull_request,
45 def _get_status_query(self, repo, revision, pull_request,
47 with_revisions=False):
46 with_revisions=False):
48 repo = self._get_repo(repo)
47 repo = self._get_repo(repo)
49
48
50 q = ChangesetStatus.query()\
49 q = ChangesetStatus.query()\
51 .filter(ChangesetStatus.repo == repo)
50 .filter(ChangesetStatus.repo == repo)
52 if not with_revisions:
51 if not with_revisions:
53 q = q.filter(ChangesetStatus.version == 0)
52 q = q.filter(ChangesetStatus.version == 0)
54
53
55 if revision:
54 if revision:
56 q = q.filter(ChangesetStatus.revision == revision)
55 q = q.filter(ChangesetStatus.revision == revision)
57 elif pull_request:
56 elif pull_request:
58 pull_request = self.__get_pull_request(pull_request)
57 pull_request = self.__get_pull_request(pull_request)
59 # TODO: johbo: Think about the impact of this join, there must
58 # TODO: johbo: Think about the impact of this join, there must
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
59 # be a reason why ChangesetStatus and ChanagesetComment is linked
61 # to the pull request. Might be that we want to do the same for
60 # to the pull request. Might be that we want to do the same for
62 # the pull_request_version_id.
61 # the pull_request_version_id.
63 q = q.join(ChangesetComment).filter(
62 q = q.join(ChangesetComment).filter(
64 ChangesetStatus.pull_request == pull_request,
63 ChangesetStatus.pull_request == pull_request,
65 ChangesetComment.pull_request_version_id == None)
64 ChangesetComment.pull_request_version_id == None)
66 else:
65 else:
67 raise Exception('Please specify revision or pull_request')
66 raise Exception('Please specify revision or pull_request')
68 q = q.order_by(ChangesetStatus.version.asc())
67 q = q.order_by(ChangesetStatus.version.asc())
69 return q
68 return q
70
69
71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
70 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
72 trim_votes=True):
71 trim_votes=True):
73 """
72 """
74 Calculate status based on given group members, and voting rule
73 Calculate status based on given group members, and voting rule
75
74
76
75
77 group1 - 4 members, 3 required for approval
76 group1 - 4 members, 3 required for approval
78 user1 - approved
77 user1 - approved
79 user2 - reject
78 user2 - reject
80 user3 - approved
79 user3 - approved
81 user4 - rejected
80 user4 - rejected
82
81
83 final_state: rejected, reasons not at least 3 votes
82 final_state: rejected, reasons not at least 3 votes
84
83
85
84
86 group1 - 4 members, 2 required for approval
85 group1 - 4 members, 2 required for approval
87 user1 - approved
86 user1 - approved
88 user2 - reject
87 user2 - reject
89 user3 - approved
88 user3 - approved
90 user4 - rejected
89 user4 - rejected
91
90
92 final_state: approved, reasons got at least 2 approvals
91 final_state: approved, reasons got at least 2 approvals
93
92
94 group1 - 4 members, ALL required for approval
93 group1 - 4 members, ALL required for approval
95 user1 - approved
94 user1 - approved
96 user2 - reject
95 user2 - reject
97 user3 - approved
96 user3 - approved
98 user4 - rejected
97 user4 - rejected
99
98
100 final_state: rejected, reasons not all approvals
99 final_state: rejected, reasons not all approvals
101
100
102
101
103 group1 - 4 members, ALL required for approval
102 group1 - 4 members, ALL required for approval
104 user1 - approved
103 user1 - approved
105 user2 - approved
104 user2 - approved
106 user3 - approved
105 user3 - approved
107 user4 - approved
106 user4 - approved
108
107
109 final_state: approved, reason all approvals received
108 final_state: approved, reason all approvals received
110
109
111 group1 - 4 members, 5 required for approval
110 group1 - 4 members, 5 required for approval
112 (approval should be shorted to number of actual members)
111 (approval should be shorted to number of actual members)
113
112
114 user1 - approved
113 user1 - approved
115 user2 - approved
114 user2 - approved
116 user3 - approved
115 user3 - approved
117 user4 - approved
116 user4 - approved
118
117
119 final_state: approved, reason all approvals received
118 final_state: approved, reason all approvals received
120
119
121 """
120 """
122 group_vote_data = {}
121 group_vote_data = {}
123 got_rule = False
122 got_rule = False
124 members = collections.OrderedDict()
123 members = collections.OrderedDict()
125 for review_obj, user, reasons, mandatory, statuses \
124 for review_obj, user, reasons, mandatory, statuses \
126 in group_statuses_by_reviewers:
125 in group_statuses_by_reviewers:
127
126
128 if not got_rule:
127 if not got_rule:
129 group_vote_data = review_obj.rule_user_group_data()
128 group_vote_data = review_obj.rule_user_group_data()
130 got_rule = bool(group_vote_data)
129 got_rule = bool(group_vote_data)
131
130
132 members[user.user_id] = statuses
131 members[user.user_id] = statuses
133
132
134 if not group_vote_data:
133 if not group_vote_data:
135 return []
134 return []
136
135
137 required_votes = group_vote_data['vote_rule']
136 required_votes = group_vote_data['vote_rule']
138 if required_votes == -1:
137 if required_votes == -1:
139 # -1 means all required, so we replace it with how many people
138 # -1 means all required, so we replace it with how many people
140 # are in the members
139 # are in the members
141 required_votes = len(members)
140 required_votes = len(members)
142
141
143 if trim_votes and required_votes > len(members):
142 if trim_votes and required_votes > len(members):
144 # we require more votes than we have members in the group
143 # we require more votes than we have members in the group
145 # in this case we trim the required votes to the number of members
144 # in this case we trim the required votes to the number of members
146 required_votes = len(members)
145 required_votes = len(members)
147
146
148 approvals = sum([
147 approvals = sum([
149 1 for statuses in members.values()
148 1 for statuses in members.values()
150 if statuses and
149 if statuses and
151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
150 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
152
151
153 calculated_votes = []
152 calculated_votes = []
154 # we have all votes from users, now check if we have enough votes
153 # we have all votes from users, now check if we have enough votes
155 # to fill other
154 # to fill other
156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
155 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
157 if approvals >= required_votes:
156 if approvals >= required_votes:
158 fill_in = ChangesetStatus.STATUS_APPROVED
157 fill_in = ChangesetStatus.STATUS_APPROVED
159
158
160 for member, statuses in members.items():
159 for member, statuses in members.items():
161 if statuses:
160 if statuses:
162 ver, latest = statuses[0]
161 ver, latest = statuses[0]
163 if fill_in == ChangesetStatus.STATUS_APPROVED:
162 if fill_in == ChangesetStatus.STATUS_APPROVED:
164 calculated_votes.append(fill_in)
163 calculated_votes.append(fill_in)
165 else:
164 else:
166 calculated_votes.append(latest.status)
165 calculated_votes.append(latest.status)
167 else:
166 else:
168 calculated_votes.append(fill_in)
167 calculated_votes.append(fill_in)
169
168
170 return calculated_votes
169 return calculated_votes
171
170
172 def calculate_status(self, statuses_by_reviewers):
171 def calculate_status(self, statuses_by_reviewers):
173 """
172 """
174 Given the approval statuses from reviewers, calculates final approval
173 Given the approval statuses from reviewers, calculates final approval
175 status. There can only be 3 results, all approved, all rejected. If
174 status. There can only be 3 results, all approved, all rejected. If
176 there is no consensus the PR is under review.
175 there is no consensus the PR is under review.
177
176
178 :param statuses_by_reviewers:
177 :param statuses_by_reviewers:
179 """
178 """
180
179
181 def group_rule(element):
180 def group_rule(element):
182 review_obj = element[0]
181 review_obj = element[0]
183 rule_data = review_obj.rule_user_group_data()
182 rule_data = review_obj.rule_user_group_data()
184 if rule_data and rule_data['id']:
183 if rule_data and rule_data['id']:
185 return rule_data['id']
184 return rule_data['id']
186
185
187 voting_groups = itertools.groupby(
186 voting_groups = itertools.groupby(
188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
187 sorted(statuses_by_reviewers, key=group_rule), group_rule)
189
188
190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
189 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
191
190
192 reviewers_number = len(statuses_by_reviewers)
191 reviewers_number = len(statuses_by_reviewers)
193 votes = collections.defaultdict(int)
192 votes = collections.defaultdict(int)
194 for group, group_statuses_by_reviewers in voting_by_groups:
193 for group, group_statuses_by_reviewers in voting_by_groups:
195 if group:
194 if group:
196 # calculate how the "group" voted
195 # calculate how the "group" voted
197 for vote_status in self.calculate_group_vote(
196 for vote_status in self.calculate_group_vote(
198 group, group_statuses_by_reviewers):
197 group, group_statuses_by_reviewers):
199 votes[vote_status] += 1
198 votes[vote_status] += 1
200 else:
199 else:
201
200
202 for review_obj, user, reasons, mandatory, statuses \
201 for review_obj, user, reasons, mandatory, statuses \
203 in group_statuses_by_reviewers:
202 in group_statuses_by_reviewers:
204 # individual vote
203 # individual vote
205 if statuses:
204 if statuses:
206 ver, latest = statuses[0]
205 ver, latest = statuses[0]
207 votes[latest.status] += 1
206 votes[latest.status] += 1
208
207
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
208 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
209 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
211
210
212 # TODO(marcink): with group voting, how does rejected work,
211 # TODO(marcink): with group voting, how does rejected work,
213 # do we ever get rejected state ?
212 # do we ever get rejected state ?
214
213
215 if approved_votes_count and (approved_votes_count == reviewers_number):
214 if approved_votes_count and (approved_votes_count == reviewers_number):
216 return ChangesetStatus.STATUS_APPROVED
215 return ChangesetStatus.STATUS_APPROVED
217
216
218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
217 if rejected_votes_count and (rejected_votes_count == reviewers_number):
219 return ChangesetStatus.STATUS_REJECTED
218 return ChangesetStatus.STATUS_REJECTED
220
219
221 return ChangesetStatus.STATUS_UNDER_REVIEW
220 return ChangesetStatus.STATUS_UNDER_REVIEW
222
221
223 def get_statuses(self, repo, revision=None, pull_request=None,
222 def get_statuses(self, repo, revision=None, pull_request=None,
224 with_revisions=False):
223 with_revisions=False):
225 q = self._get_status_query(repo, revision, pull_request,
224 q = self._get_status_query(repo, revision, pull_request,
226 with_revisions)
225 with_revisions)
227 return q.all()
226 return q.all()
228
227
229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
228 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
230 """
229 """
231 Returns latest status of changeset for given revision or for given
230 Returns latest status of changeset for given revision or for given
232 pull request. Statuses are versioned inside a table itself and
231 pull request. Statuses are versioned inside a table itself and
233 version == 0 is always the current one
232 version == 0 is always the current one
234
233
235 :param repo:
234 :param repo:
236 :param revision: 40char hash or None
235 :param revision: 40char hash or None
237 :param pull_request: pull_request reference
236 :param pull_request: pull_request reference
238 :param as_str: return status as string not object
237 :param as_str: return status as string not object
239 """
238 """
240 q = self._get_status_query(repo, revision, pull_request)
239 q = self._get_status_query(repo, revision, pull_request)
241
240
242 # need to use first here since there can be multiple statuses
241 # need to use first here since there can be multiple statuses
243 # returned from pull_request
242 # returned from pull_request
244 status = q.first()
243 status = q.first()
245 if as_str:
244 if as_str:
246 status = status.status if status else status
245 status = status.status if status else status
247 st = status or ChangesetStatus.DEFAULT
246 st = status or ChangesetStatus.DEFAULT
248 return str(st)
247 return str(st)
249 return status
248 return status
250
249
251 def _render_auto_status_message(
250 def _render_auto_status_message(
252 self, status, commit_id=None, pull_request=None):
251 self, status, commit_id=None, pull_request=None):
253 """
252 """
254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
253 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
255 so it's always looking the same disregarding on which default
254 so it's always looking the same disregarding on which default
256 renderer system is using.
255 renderer system is using.
257
256
258 :param status: status text to change into
257 :param status: status text to change into
259 :param commit_id: the commit_id we change the status for
258 :param commit_id: the commit_id we change the status for
260 :param pull_request: the pull request we change the status for
259 :param pull_request: the pull request we change the status for
261 """
260 """
262
261
263 new_status = ChangesetStatus.get_status_lbl(status)
262 new_status = ChangesetStatus.get_status_lbl(status)
264
263
265 params = {
264 params = {
266 'new_status_label': new_status,
265 'new_status_label': new_status,
267 'pull_request': pull_request,
266 'pull_request': pull_request,
268 'commit_id': commit_id,
267 'commit_id': commit_id,
269 }
268 }
270 renderer = RstTemplateRenderer()
269 renderer = RstTemplateRenderer()
271 return renderer.render('auto_status_change.mako', **params)
270 return renderer.render('auto_status_change.mako', **params)
272
271
273 def set_status(self, repo, status, user, comment=None, revision=None,
272 def set_status(self, repo, status, user, comment=None, revision=None,
274 pull_request=None, dont_allow_on_closed_pull_request=False):
273 pull_request=None, dont_allow_on_closed_pull_request=False):
275 """
274 """
276 Creates new status for changeset or updates the old ones bumping their
275 Creates new status for changeset or updates the old ones bumping their
277 version, leaving the current status at
276 version, leaving the current status at
278
277
279 :param repo:
278 :param repo:
280 :param revision:
279 :param revision:
281 :param status:
280 :param status:
282 :param user:
281 :param user:
283 :param comment:
282 :param comment:
284 :param dont_allow_on_closed_pull_request: don't allow a status change
283 :param dont_allow_on_closed_pull_request: don't allow a status change
285 if last status was for pull request and it's closed. We shouldn't
284 if last status was for pull request and it's closed. We shouldn't
286 mess around this manually
285 mess around this manually
287 """
286 """
288 repo = self._get_repo(repo)
287 repo = self._get_repo(repo)
289
288
290 q = ChangesetStatus.query()
289 q = ChangesetStatus.query()
291
290
292 if revision:
291 if revision:
293 q = q.filter(ChangesetStatus.repo == repo)
292 q = q.filter(ChangesetStatus.repo == repo)
294 q = q.filter(ChangesetStatus.revision == revision)
293 q = q.filter(ChangesetStatus.revision == revision)
295 elif pull_request:
294 elif pull_request:
296 pull_request = self.__get_pull_request(pull_request)
295 pull_request = self.__get_pull_request(pull_request)
297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
296 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
297 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
299 cur_statuses = q.all()
298 cur_statuses = q.all()
300
299
301 # if statuses exists and last is associated with a closed pull request
300 # if statuses exists and last is associated with a closed pull request
302 # we need to check if we can allow this status change
301 # we need to check if we can allow this status change
303 if (dont_allow_on_closed_pull_request and cur_statuses
302 if (dont_allow_on_closed_pull_request and cur_statuses
304 and getattr(cur_statuses[0].pull_request, 'status', '')
303 and getattr(cur_statuses[0].pull_request, 'status', '')
305 == PullRequest.STATUS_CLOSED):
304 == PullRequest.STATUS_CLOSED):
306 raise StatusChangeOnClosedPullRequestError(
305 raise StatusChangeOnClosedPullRequestError(
307 'Changing status on closed pull request is not allowed'
306 'Changing status on closed pull request is not allowed'
308 )
307 )
309
308
310 # update all current statuses with older version
309 # update all current statuses with older version
311 if cur_statuses:
310 if cur_statuses:
312 for st in cur_statuses:
311 for st in cur_statuses:
313 st.version += 1
312 st.version += 1
314 Session().add(st)
313 Session().add(st)
315 Session().flush()
314 Session().flush()
316
315
317 def _create_status(user, repo, status, comment, revision, pull_request):
316 def _create_status(user, repo, status, comment, revision, pull_request):
318 new_status = ChangesetStatus()
317 new_status = ChangesetStatus()
319 new_status.author = self._get_user(user)
318 new_status.author = self._get_user(user)
320 new_status.repo = self._get_repo(repo)
319 new_status.repo = self._get_repo(repo)
321 new_status.status = status
320 new_status.status = status
322 new_status.comment = comment
321 new_status.comment = comment
323 new_status.revision = revision
322 new_status.revision = revision
324 new_status.pull_request = pull_request
323 new_status.pull_request = pull_request
325 return new_status
324 return new_status
326
325
327 if not comment:
326 if not comment:
328 from rhodecode.model.comment import CommentsModel
327 from rhodecode.model.comment import CommentsModel
329 comment = CommentsModel().create(
328 comment = CommentsModel().create(
330 text=self._render_auto_status_message(
329 text=self._render_auto_status_message(
331 status, commit_id=revision, pull_request=pull_request),
330 status, commit_id=revision, pull_request=pull_request),
332 repo=repo,
331 repo=repo,
333 user=user,
332 user=user,
334 pull_request=pull_request,
333 pull_request=pull_request,
335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
334 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
336 )
335 )
337
336
338 if revision:
337 if revision:
339 new_status = _create_status(
338 new_status = _create_status(
340 user=user, repo=repo, status=status, comment=comment,
339 user=user, repo=repo, status=status, comment=comment,
341 revision=revision, pull_request=pull_request)
340 revision=revision, pull_request=pull_request)
342 Session().add(new_status)
341 Session().add(new_status)
343 return new_status
342 return new_status
344 elif pull_request:
343 elif pull_request:
345 # pull request can have more than one revision associated to it
344 # pull request can have more than one revision associated to it
346 # we need to create new version for each one
345 # we need to create new version for each one
347 new_statuses = []
346 new_statuses = []
348 repo = pull_request.source_repo
347 repo = pull_request.source_repo
349 for rev in pull_request.revisions:
348 for rev in pull_request.revisions:
350 new_status = _create_status(
349 new_status = _create_status(
351 user=user, repo=repo, status=status, comment=comment,
350 user=user, repo=repo, status=status, comment=comment,
352 revision=rev, pull_request=pull_request)
351 revision=rev, pull_request=pull_request)
353 new_statuses.append(new_status)
352 new_statuses.append(new_status)
354 Session().add(new_status)
353 Session().add(new_status)
355 return new_statuses
354 return new_statuses
356
355
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data, user=None):
356 def aggregate_votes_by_user(self, commit_statuses, reviewers_data, user=None):
358
357
359 commit_statuses_map = collections.defaultdict(list)
358 commit_statuses_map = collections.defaultdict(list)
360 for st in commit_statuses:
359 for st in commit_statuses:
361 commit_statuses_map[st.author.username] += [st]
360 commit_statuses_map[st.author.username] += [st]
362
361
363 reviewers = []
362 reviewers = []
364
363
365 def version(commit_status):
364 def version(commit_status):
366 return commit_status.version
365 return commit_status.version
367
366
368 for obj in reviewers_data:
367 for obj in reviewers_data:
369 if not obj.user:
368 if not obj.user:
370 continue
369 continue
371 if user and obj.user.username != user.username:
370 if user and obj.user.username != user.username:
372 # single user filter
371 # single user filter
373 continue
372 continue
374
373
375 statuses = commit_statuses_map.get(obj.user.username, None)
374 statuses = commit_statuses_map.get(obj.user.username, None)
376 if statuses:
375 if statuses:
377 status_groups = itertools.groupby(
376 status_groups = itertools.groupby(
378 sorted(statuses, key=version), version)
377 sorted(statuses, key=version), version)
379 statuses = [(x, list(y)[0]) for x, y in status_groups]
378 statuses = [(x, list(y)[0]) for x, y in status_groups]
380
379
381 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
380 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
382
381
383 if user:
382 if user:
384 return reviewers[0] if reviewers else reviewers
383 return reviewers[0] if reviewers else reviewers
385 else:
384 else:
386 return reviewers
385 return reviewers
387
386
388 def reviewers_statuses(self, pull_request, user=None):
387 def reviewers_statuses(self, pull_request, user=None):
389 _commit_statuses = self.get_statuses(
388 _commit_statuses = self.get_statuses(
390 pull_request.source_repo,
389 pull_request.source_repo,
391 pull_request=pull_request,
390 pull_request=pull_request,
392 with_revisions=True)
391 with_revisions=True)
393 reviewers = pull_request.get_pull_request_reviewers(
392 reviewers = pull_request.get_pull_request_reviewers(
394 role=PullRequestReviewers.ROLE_REVIEWER)
393 role=PullRequestReviewers.ROLE_REVIEWER)
395 return self.aggregate_votes_by_user(_commit_statuses, reviewers, user=user)
394 return self.aggregate_votes_by_user(_commit_statuses, reviewers, user=user)
396
395
397 def calculated_review_status(self, pull_request):
396 def calculated_review_status(self, pull_request):
398 """
397 """
399 calculate pull request status based on reviewers, it should be a list
398 calculate pull request status based on reviewers, it should be a list
400 of two element lists.
399 of two element lists.
401 """
400 """
402 reviewers = self.reviewers_statuses(pull_request)
401 reviewers = self.reviewers_statuses(pull_request)
403 return self.calculate_status(reviewers)
402 return self.calculate_status(reviewers)
@@ -1,857 +1,857 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 false, true,
40 false, true,
41 ChangesetComment,
41 ChangesetComment,
42 User,
42 User,
43 Notification,
43 Notification,
44 PullRequest,
44 PullRequest,
45 AttributeDict,
45 AttributeDict,
46 ChangesetCommentHistory,
46 ChangesetCommentHistory,
47 )
47 )
48 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.notification import NotificationModel
49 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
50 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.notification import EmailNotificationModel
52 from rhodecode.model.validation_schema.schemas import comment_schema
52 from rhodecode.model.validation_schema.schemas import comment_schema
53
53
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class CommentsModel(BaseModel):
58 class CommentsModel(BaseModel):
59
59
60 cls = ChangesetComment
60 cls = ChangesetComment
61
61
62 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_BEFORE = 3
63 DIFF_CONTEXT_AFTER = 3
63 DIFF_CONTEXT_AFTER = 3
64
64
65 def __get_commit_comment(self, changeset_comment):
65 def __get_commit_comment(self, changeset_comment):
66 return self._get_instance(ChangesetComment, changeset_comment)
66 return self._get_instance(ChangesetComment, changeset_comment)
67
67
68 def __get_pull_request(self, pull_request):
68 def __get_pull_request(self, pull_request):
69 return self._get_instance(PullRequest, pull_request)
69 return self._get_instance(PullRequest, pull_request)
70
70
71 def _extract_mentions(self, s):
71 def _extract_mentions(self, s):
72 user_objects = []
72 user_objects = []
73 for username in extract_mentioned_users(s):
73 for username in extract_mentioned_users(s):
74 user_obj = User.get_by_username(username, case_insensitive=True)
74 user_obj = User.get_by_username(username, case_insensitive=True)
75 if user_obj:
75 if user_obj:
76 user_objects.append(user_obj)
76 user_objects.append(user_obj)
77 return user_objects
77 return user_objects
78
78
79 def _get_renderer(self, global_renderer='rst', request=None):
79 def _get_renderer(self, global_renderer='rst', request=None):
80 request = request or get_current_request()
80 request = request or get_current_request()
81
81
82 try:
82 try:
83 global_renderer = request.call_context.visual.default_renderer
83 global_renderer = request.call_context.visual.default_renderer
84 except AttributeError:
84 except AttributeError:
85 log.debug("Renderer not set, falling back "
85 log.debug("Renderer not set, falling back "
86 "to default renderer '%s'", global_renderer)
86 "to default renderer '%s'", global_renderer)
87 except Exception:
87 except Exception:
88 log.error(traceback.format_exc())
88 log.error(traceback.format_exc())
89 return global_renderer
89 return global_renderer
90
90
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
92 # group by versions, and count until, and display objects
92 # group by versions, and count until, and display objects
93
93
94 comment_groups = collections.defaultdict(list)
94 comment_groups = collections.defaultdict(list)
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
96
96
97 def yield_comments(pos):
97 def yield_comments(pos):
98 for co in comment_groups[pos]:
98 for co in comment_groups[pos]:
99 yield co
99 yield co
100
100
101 comment_versions = collections.defaultdict(
101 comment_versions = collections.defaultdict(
102 lambda: collections.defaultdict(list))
102 lambda: collections.defaultdict(list))
103 prev_prvid = -1
103 prev_prvid = -1
104 # fake last entry with None, to aggregate on "latest" version which
104 # fake last entry with None, to aggregate on "latest" version which
105 # doesn't have an pull_request_version_id
105 # doesn't have an pull_request_version_id
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 prvid = ver.pull_request_version_id
107 prvid = ver.pull_request_version_id
108 if prev_prvid == -1:
108 if prev_prvid == -1:
109 prev_prvid = prvid
109 prev_prvid = prvid
110
110
111 for co in yield_comments(prvid):
111 for co in yield_comments(prvid):
112 comment_versions[prvid]['at'].append(co)
112 comment_versions[prvid]['at'].append(co)
113
113
114 # save until
114 # save until
115 current = comment_versions[prvid]['at']
115 current = comment_versions[prvid]['at']
116 prev_until = comment_versions[prev_prvid]['until']
116 prev_until = comment_versions[prev_prvid]['until']
117 cur_until = prev_until + current
117 cur_until = prev_until + current
118 comment_versions[prvid]['until'].extend(cur_until)
118 comment_versions[prvid]['until'].extend(cur_until)
119
119
120 # save outdated
120 # save outdated
121 if inline:
121 if inline:
122 outdated = [x for x in cur_until
122 outdated = [x for x in cur_until
123 if x.outdated_at_version(show_version)]
123 if x.outdated_at_version(show_version)]
124 else:
124 else:
125 outdated = [x for x in cur_until
125 outdated = [x for x in cur_until
126 if x.older_than_version(show_version)]
126 if x.older_than_version(show_version)]
127 display = [x for x in cur_until if x not in outdated]
127 display = [x for x in cur_until if x not in outdated]
128
128
129 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['outdated'] = outdated
130 comment_versions[prvid]['display'] = display
130 comment_versions[prvid]['display'] = display
131
131
132 prev_prvid = prvid
132 prev_prvid = prvid
133
133
134 return comment_versions
134 return comment_versions
135
135
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 qry = Session().query(ChangesetComment) \
137 qry = Session().query(ChangesetComment) \
138 .filter(ChangesetComment.repo == repo)
138 .filter(ChangesetComment.repo == repo)
139
139
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142
142
143 if user:
143 if user:
144 user = self._get_user(user)
144 user = self._get_user(user)
145 if user:
145 if user:
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147
147
148 if commit_id:
148 if commit_id:
149 qry = qry.filter(ChangesetComment.revision == commit_id)
149 qry = qry.filter(ChangesetComment.revision == commit_id)
150
150
151 qry = qry.order_by(ChangesetComment.created_on)
151 qry = qry.order_by(ChangesetComment.created_on)
152 return qry.all()
152 return qry.all()
153
153
154 def get_repository_unresolved_todos(self, repo):
154 def get_repository_unresolved_todos(self, repo):
155 todos = Session().query(ChangesetComment) \
155 todos = Session().query(ChangesetComment) \
156 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.repo == repo) \
157 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.resolved_by == None) \
158 .filter(ChangesetComment.comment_type
158 .filter(ChangesetComment.comment_type
159 == ChangesetComment.COMMENT_TYPE_TODO)
159 == ChangesetComment.COMMENT_TYPE_TODO)
160 todos = todos.all()
160 todos = todos.all()
161
161
162 return todos
162 return todos
163
163
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
165
165
166 todos = Session().query(ChangesetComment) \
166 todos = Session().query(ChangesetComment) \
167 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.pull_request == pull_request) \
168 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.resolved_by == None) \
169 .filter(ChangesetComment.comment_type
169 .filter(ChangesetComment.comment_type
170 == ChangesetComment.COMMENT_TYPE_TODO)
170 == ChangesetComment.COMMENT_TYPE_TODO)
171
171
172 if not include_drafts:
172 if not include_drafts:
173 todos = todos.filter(ChangesetComment.draft == false())
173 todos = todos.filter(ChangesetComment.draft == false())
174
174
175 if not show_outdated:
175 if not show_outdated:
176 todos = todos.filter(
176 todos = todos.filter(
177 coalesce(ChangesetComment.display_state, '') !=
177 coalesce(ChangesetComment.display_state, '') !=
178 ChangesetComment.COMMENT_OUTDATED)
178 ChangesetComment.COMMENT_OUTDATED)
179
179
180 todos = todos.all()
180 todos = todos.all()
181
181
182 return todos
182 return todos
183
183
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
185
185
186 todos = Session().query(ChangesetComment) \
186 todos = Session().query(ChangesetComment) \
187 .filter(ChangesetComment.pull_request == pull_request) \
187 .filter(ChangesetComment.pull_request == pull_request) \
188 .filter(ChangesetComment.resolved_by != None) \
188 .filter(ChangesetComment.resolved_by != None) \
189 .filter(ChangesetComment.comment_type
189 .filter(ChangesetComment.comment_type
190 == ChangesetComment.COMMENT_TYPE_TODO)
190 == ChangesetComment.COMMENT_TYPE_TODO)
191
191
192 if not include_drafts:
192 if not include_drafts:
193 todos = todos.filter(ChangesetComment.draft == false())
193 todos = todos.filter(ChangesetComment.draft == false())
194
194
195 if not show_outdated:
195 if not show_outdated:
196 todos = todos.filter(
196 todos = todos.filter(
197 coalesce(ChangesetComment.display_state, '') !=
197 coalesce(ChangesetComment.display_state, '') !=
198 ChangesetComment.COMMENT_OUTDATED)
198 ChangesetComment.COMMENT_OUTDATED)
199
199
200 todos = todos.all()
200 todos = todos.all()
201
201
202 return todos
202 return todos
203
203
204 def get_pull_request_drafts(self, user_id, pull_request):
204 def get_pull_request_drafts(self, user_id, pull_request):
205 drafts = Session().query(ChangesetComment) \
205 drafts = Session().query(ChangesetComment) \
206 .filter(ChangesetComment.pull_request == pull_request) \
206 .filter(ChangesetComment.pull_request == pull_request) \
207 .filter(ChangesetComment.user_id == user_id) \
207 .filter(ChangesetComment.user_id == user_id) \
208 .filter(ChangesetComment.draft == true())
208 .filter(ChangesetComment.draft == true())
209 return drafts.all()
209 return drafts.all()
210
210
211 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
211 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
212
212
213 todos = Session().query(ChangesetComment) \
213 todos = Session().query(ChangesetComment) \
214 .filter(ChangesetComment.revision == commit_id) \
214 .filter(ChangesetComment.revision == commit_id) \
215 .filter(ChangesetComment.resolved_by == None) \
215 .filter(ChangesetComment.resolved_by == None) \
216 .filter(ChangesetComment.comment_type
216 .filter(ChangesetComment.comment_type
217 == ChangesetComment.COMMENT_TYPE_TODO)
217 == ChangesetComment.COMMENT_TYPE_TODO)
218
218
219 if not include_drafts:
219 if not include_drafts:
220 todos = todos.filter(ChangesetComment.draft == false())
220 todos = todos.filter(ChangesetComment.draft == false())
221
221
222 if not show_outdated:
222 if not show_outdated:
223 todos = todos.filter(
223 todos = todos.filter(
224 coalesce(ChangesetComment.display_state, '') !=
224 coalesce(ChangesetComment.display_state, '') !=
225 ChangesetComment.COMMENT_OUTDATED)
225 ChangesetComment.COMMENT_OUTDATED)
226
226
227 todos = todos.all()
227 todos = todos.all()
228
228
229 return todos
229 return todos
230
230
231 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
231 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
232
232
233 todos = Session().query(ChangesetComment) \
233 todos = Session().query(ChangesetComment) \
234 .filter(ChangesetComment.revision == commit_id) \
234 .filter(ChangesetComment.revision == commit_id) \
235 .filter(ChangesetComment.resolved_by != None) \
235 .filter(ChangesetComment.resolved_by != None) \
236 .filter(ChangesetComment.comment_type
236 .filter(ChangesetComment.comment_type
237 == ChangesetComment.COMMENT_TYPE_TODO)
237 == ChangesetComment.COMMENT_TYPE_TODO)
238
238
239 if not include_drafts:
239 if not include_drafts:
240 todos = todos.filter(ChangesetComment.draft == false())
240 todos = todos.filter(ChangesetComment.draft == false())
241
241
242 if not show_outdated:
242 if not show_outdated:
243 todos = todos.filter(
243 todos = todos.filter(
244 coalesce(ChangesetComment.display_state, '') !=
244 coalesce(ChangesetComment.display_state, '') !=
245 ChangesetComment.COMMENT_OUTDATED)
245 ChangesetComment.COMMENT_OUTDATED)
246
246
247 todos = todos.all()
247 todos = todos.all()
248
248
249 return todos
249 return todos
250
250
251 def get_commit_inline_comments(self, commit_id, include_drafts=True):
251 def get_commit_inline_comments(self, commit_id, include_drafts=True):
252 inline_comments = Session().query(ChangesetComment) \
252 inline_comments = Session().query(ChangesetComment) \
253 .filter(ChangesetComment.line_no != None) \
253 .filter(ChangesetComment.line_no != None) \
254 .filter(ChangesetComment.f_path != None) \
254 .filter(ChangesetComment.f_path != None) \
255 .filter(ChangesetComment.revision == commit_id)
255 .filter(ChangesetComment.revision == commit_id)
256
256
257 if not include_drafts:
257 if not include_drafts:
258 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
258 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
259
259
260 inline_comments = inline_comments.all()
260 inline_comments = inline_comments.all()
261 return inline_comments
261 return inline_comments
262
262
263 def _log_audit_action(self, action, action_data, auth_user, comment):
263 def _log_audit_action(self, action, action_data, auth_user, comment):
264 audit_logger.store(
264 audit_logger.store(
265 action=action,
265 action=action,
266 action_data=action_data,
266 action_data=action_data,
267 user=auth_user,
267 user=auth_user,
268 repo=comment.repo)
268 repo=comment.repo)
269
269
270 def create(self, text, repo, user, commit_id=None, pull_request=None,
270 def create(self, text, repo, user, commit_id=None, pull_request=None,
271 f_path=None, line_no=None, status_change=None,
271 f_path=None, line_no=None, status_change=None,
272 status_change_type=None, comment_type=None, is_draft=False,
272 status_change_type=None, comment_type=None, is_draft=False,
273 resolves_comment_id=None, closing_pr=False, send_email=True,
273 resolves_comment_id=None, closing_pr=False, send_email=True,
274 renderer=None, auth_user=None, extra_recipients=None):
274 renderer=None, auth_user=None, extra_recipients=None):
275 """
275 """
276 Creates new comment for commit or pull request.
276 Creates new comment for commit or pull request.
277 IF status_change is not none this comment is associated with a
277 IF status_change is not none this comment is associated with a
278 status change of commit or commit associated with pull request
278 status change of commit or commit associated with pull request
279
279
280 :param text:
280 :param text:
281 :param repo:
281 :param repo:
282 :param user:
282 :param user:
283 :param commit_id:
283 :param commit_id:
284 :param pull_request:
284 :param pull_request:
285 :param f_path:
285 :param f_path:
286 :param line_no:
286 :param line_no:
287 :param status_change: Label for status change
287 :param status_change: Label for status change
288 :param comment_type: Type of comment
288 :param comment_type: Type of comment
289 :param is_draft: is comment a draft only
289 :param is_draft: is comment a draft only
290 :param resolves_comment_id: id of comment which this one will resolve
290 :param resolves_comment_id: id of comment which this one will resolve
291 :param status_change_type: type of status change
291 :param status_change_type: type of status change
292 :param closing_pr:
292 :param closing_pr:
293 :param send_email:
293 :param send_email:
294 :param renderer: pick renderer for this comment
294 :param renderer: pick renderer for this comment
295 :param auth_user: current authenticated user calling this method
295 :param auth_user: current authenticated user calling this method
296 :param extra_recipients: list of extra users to be added to recipients
296 :param extra_recipients: list of extra users to be added to recipients
297 """
297 """
298
298
299 if not text:
299 if not text:
300 log.warning('Missing text for comment, skipping...')
300 log.warning('Missing text for comment, skipping...')
301 return
301 return
302 request = get_current_request()
302 request = get_current_request()
303 _ = request.translate
303 _ = request.translate
304
304
305 if not renderer:
305 if not renderer:
306 renderer = self._get_renderer(request=request)
306 renderer = self._get_renderer(request=request)
307
307
308 repo = self._get_repo(repo)
308 repo = self._get_repo(repo)
309 user = self._get_user(user)
309 user = self._get_user(user)
310 auth_user = auth_user or user
310 auth_user = auth_user or user
311
311
312 schema = comment_schema.CommentSchema()
312 schema = comment_schema.CommentSchema()
313 validated_kwargs = schema.deserialize(dict(
313 validated_kwargs = schema.deserialize(dict(
314 comment_body=text,
314 comment_body=text,
315 comment_type=comment_type,
315 comment_type=comment_type,
316 is_draft=is_draft,
316 is_draft=is_draft,
317 comment_file=f_path,
317 comment_file=f_path,
318 comment_line=line_no,
318 comment_line=line_no,
319 renderer_type=renderer,
319 renderer_type=renderer,
320 status_change=status_change_type,
320 status_change=status_change_type,
321 resolves_comment_id=resolves_comment_id,
321 resolves_comment_id=resolves_comment_id,
322 repo=repo.repo_id,
322 repo=repo.repo_id,
323 user=user.user_id,
323 user=user.user_id,
324 ))
324 ))
325 is_draft = validated_kwargs['is_draft']
325 is_draft = validated_kwargs['is_draft']
326
326
327 comment = ChangesetComment()
327 comment = ChangesetComment()
328 comment.renderer = validated_kwargs['renderer_type']
328 comment.renderer = validated_kwargs['renderer_type']
329 comment.text = validated_kwargs['comment_body']
329 comment.text = validated_kwargs['comment_body']
330 comment.f_path = validated_kwargs['comment_file']
330 comment.f_path = validated_kwargs['comment_file']
331 comment.line_no = validated_kwargs['comment_line']
331 comment.line_no = validated_kwargs['comment_line']
332 comment.comment_type = validated_kwargs['comment_type']
332 comment.comment_type = validated_kwargs['comment_type']
333 comment.draft = is_draft
333 comment.draft = is_draft
334
334
335 comment.repo = repo
335 comment.repo = repo
336 comment.author = user
336 comment.author = user
337 resolved_comment = self.__get_commit_comment(
337 resolved_comment = self.__get_commit_comment(
338 validated_kwargs['resolves_comment_id'])
338 validated_kwargs['resolves_comment_id'])
339
339
340 # check if the comment actually belongs to this PR
340 # check if the comment actually belongs to this PR
341 if resolved_comment and resolved_comment.pull_request and \
341 if resolved_comment and resolved_comment.pull_request and \
342 resolved_comment.pull_request != pull_request:
342 resolved_comment.pull_request != pull_request:
343 log.warning('Comment tried to resolved unrelated todo comment: %s',
343 log.warning('Comment tried to resolved unrelated todo comment: %s',
344 resolved_comment)
344 resolved_comment)
345 # comment not bound to this pull request, forbid
345 # comment not bound to this pull request, forbid
346 resolved_comment = None
346 resolved_comment = None
347
347
348 elif resolved_comment and resolved_comment.repo and \
348 elif resolved_comment and resolved_comment.repo and \
349 resolved_comment.repo != repo:
349 resolved_comment.repo != repo:
350 log.warning('Comment tried to resolved unrelated todo comment: %s',
350 log.warning('Comment tried to resolved unrelated todo comment: %s',
351 resolved_comment)
351 resolved_comment)
352 # comment not bound to this repo, forbid
352 # comment not bound to this repo, forbid
353 resolved_comment = None
353 resolved_comment = None
354
354
355 if resolved_comment and resolved_comment.resolved_by:
355 if resolved_comment and resolved_comment.resolved_by:
356 # if this comment is already resolved, don't mark it again!
356 # if this comment is already resolved, don't mark it again!
357 resolved_comment = None
357 resolved_comment = None
358
358
359 comment.resolved_comment = resolved_comment
359 comment.resolved_comment = resolved_comment
360
360
361 pull_request_id = pull_request
361 pull_request_id = pull_request
362
362
363 commit_obj = None
363 commit_obj = None
364 pull_request_obj = None
364 pull_request_obj = None
365
365
366 if commit_id:
366 if commit_id:
367 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
367 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
368 # do a lookup, so we don't pass something bad here
368 # do a lookup, so we don't pass something bad here
369 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
369 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
370 comment.revision = commit_obj.raw_id
370 comment.revision = commit_obj.raw_id
371
371
372 elif pull_request_id:
372 elif pull_request_id:
373 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
373 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
374 pull_request_obj = self.__get_pull_request(pull_request_id)
374 pull_request_obj = self.__get_pull_request(pull_request_id)
375 comment.pull_request = pull_request_obj
375 comment.pull_request = pull_request_obj
376 else:
376 else:
377 raise Exception('Please specify commit or pull_request_id')
377 raise Exception('Please specify commit or pull_request_id')
378
378
379 Session().add(comment)
379 Session().add(comment)
380 Session().flush()
380 Session().flush()
381 kwargs = {
381 kwargs = {
382 'user': user,
382 'user': user,
383 'renderer_type': renderer,
383 'renderer_type': renderer,
384 'repo_name': repo.repo_name,
384 'repo_name': repo.repo_name,
385 'status_change': status_change,
385 'status_change': status_change,
386 'status_change_type': status_change_type,
386 'status_change_type': status_change_type,
387 'comment_body': text,
387 'comment_body': text,
388 'comment_file': f_path,
388 'comment_file': f_path,
389 'comment_line': line_no,
389 'comment_line': line_no,
390 'comment_type': comment_type or 'note',
390 'comment_type': comment_type or 'note',
391 'comment_id': comment.comment_id
391 'comment_id': comment.comment_id
392 }
392 }
393
393
394 if commit_obj:
394 if commit_obj:
395 recipients = ChangesetComment.get_users(
395 recipients = ChangesetComment.get_users(
396 revision=commit_obj.raw_id)
396 revision=commit_obj.raw_id)
397 # add commit author if it's in RhodeCode system
397 # add commit author if it's in RhodeCode system
398 cs_author = User.get_from_cs_author(commit_obj.author)
398 cs_author = User.get_from_cs_author(commit_obj.author)
399 if not cs_author:
399 if not cs_author:
400 # use repo owner if we cannot extract the author correctly
400 # use repo owner if we cannot extract the author correctly
401 cs_author = repo.user
401 cs_author = repo.user
402 recipients += [cs_author]
402 recipients += [cs_author]
403
403
404 commit_comment_url = self.get_url(comment, request=request)
404 commit_comment_url = self.get_url(comment, request=request)
405 commit_comment_reply_url = self.get_url(
405 commit_comment_reply_url = self.get_url(
406 comment, request=request,
406 comment, request=request,
407 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
407 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
408
408
409 target_repo_url = h.link_to(
409 target_repo_url = h.link_to(
410 repo.repo_name,
410 repo.repo_name,
411 h.route_url('repo_summary', repo_name=repo.repo_name))
411 h.route_url('repo_summary', repo_name=repo.repo_name))
412
412
413 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
413 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
414 commit_id=commit_id)
414 commit_id=commit_id)
415
415
416 # commit specifics
416 # commit specifics
417 kwargs.update({
417 kwargs.update({
418 'commit': commit_obj,
418 'commit': commit_obj,
419 'commit_message': commit_obj.message,
419 'commit_message': commit_obj.message,
420 'commit_target_repo_url': target_repo_url,
420 'commit_target_repo_url': target_repo_url,
421 'commit_comment_url': commit_comment_url,
421 'commit_comment_url': commit_comment_url,
422 'commit_comment_reply_url': commit_comment_reply_url,
422 'commit_comment_reply_url': commit_comment_reply_url,
423 'commit_url': commit_url,
423 'commit_url': commit_url,
424 'thread_ids': [commit_url, commit_comment_url],
424 'thread_ids': [commit_url, commit_comment_url],
425 })
425 })
426
426
427 elif pull_request_obj:
427 elif pull_request_obj:
428 # get the current participants of this pull request
428 # get the current participants of this pull request
429 recipients = ChangesetComment.get_users(
429 recipients = ChangesetComment.get_users(
430 pull_request_id=pull_request_obj.pull_request_id)
430 pull_request_id=pull_request_obj.pull_request_id)
431 # add pull request author
431 # add pull request author
432 recipients += [pull_request_obj.author]
432 recipients += [pull_request_obj.author]
433
433
434 # add the reviewers to notification
434 # add the reviewers to notification
435 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
435 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
436
436
437 pr_target_repo = pull_request_obj.target_repo
437 pr_target_repo = pull_request_obj.target_repo
438 pr_source_repo = pull_request_obj.source_repo
438 pr_source_repo = pull_request_obj.source_repo
439
439
440 pr_comment_url = self.get_url(comment, request=request)
440 pr_comment_url = self.get_url(comment, request=request)
441 pr_comment_reply_url = self.get_url(
441 pr_comment_reply_url = self.get_url(
442 comment, request=request,
442 comment, request=request,
443 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
443 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
444
444
445 pr_url = h.route_url(
445 pr_url = h.route_url(
446 'pullrequest_show',
446 'pullrequest_show',
447 repo_name=pr_target_repo.repo_name,
447 repo_name=pr_target_repo.repo_name,
448 pull_request_id=pull_request_obj.pull_request_id, )
448 pull_request_id=pull_request_obj.pull_request_id, )
449
449
450 # set some variables for email notification
450 # set some variables for email notification
451 pr_target_repo_url = h.route_url(
451 pr_target_repo_url = h.route_url(
452 'repo_summary', repo_name=pr_target_repo.repo_name)
452 'repo_summary', repo_name=pr_target_repo.repo_name)
453
453
454 pr_source_repo_url = h.route_url(
454 pr_source_repo_url = h.route_url(
455 'repo_summary', repo_name=pr_source_repo.repo_name)
455 'repo_summary', repo_name=pr_source_repo.repo_name)
456
456
457 # pull request specifics
457 # pull request specifics
458 kwargs.update({
458 kwargs.update({
459 'pull_request': pull_request_obj,
459 'pull_request': pull_request_obj,
460 'pr_id': pull_request_obj.pull_request_id,
460 'pr_id': pull_request_obj.pull_request_id,
461 'pull_request_url': pr_url,
461 'pull_request_url': pr_url,
462 'pull_request_target_repo': pr_target_repo,
462 'pull_request_target_repo': pr_target_repo,
463 'pull_request_target_repo_url': pr_target_repo_url,
463 'pull_request_target_repo_url': pr_target_repo_url,
464 'pull_request_source_repo': pr_source_repo,
464 'pull_request_source_repo': pr_source_repo,
465 'pull_request_source_repo_url': pr_source_repo_url,
465 'pull_request_source_repo_url': pr_source_repo_url,
466 'pr_comment_url': pr_comment_url,
466 'pr_comment_url': pr_comment_url,
467 'pr_comment_reply_url': pr_comment_reply_url,
467 'pr_comment_reply_url': pr_comment_reply_url,
468 'pr_closing': closing_pr,
468 'pr_closing': closing_pr,
469 'thread_ids': [pr_url, pr_comment_url],
469 'thread_ids': [pr_url, pr_comment_url],
470 })
470 })
471
471
472 if send_email:
472 if send_email:
473 recipients += [self._get_user(u) for u in (extra_recipients or [])]
473 recipients += [self._get_user(u) for u in (extra_recipients or [])]
474
474
475 mention_recipients = set(
475 mention_recipients = set(
476 self._extract_mentions(text)).difference(recipients)
476 self._extract_mentions(text)).difference(recipients)
477
477
478 # create notification objects, and emails
478 # create notification objects, and emails
479 NotificationModel().create(
479 NotificationModel().create(
480 created_by=user,
480 created_by=user,
481 notification_subject='', # Filled in based on the notification_type
481 notification_subject='', # Filled in based on the notification_type
482 notification_body='', # Filled in based on the notification_type
482 notification_body='', # Filled in based on the notification_type
483 notification_type=notification_type,
483 notification_type=notification_type,
484 recipients=recipients,
484 recipients=recipients,
485 mention_recipients=mention_recipients,
485 mention_recipients=mention_recipients,
486 email_kwargs=kwargs,
486 email_kwargs=kwargs,
487 )
487 )
488
488
489 Session().flush()
489 Session().flush()
490 if comment.pull_request:
490 if comment.pull_request:
491 action = 'repo.pull_request.comment.create'
491 action = 'repo.pull_request.comment.create'
492 else:
492 else:
493 action = 'repo.commit.comment.create'
493 action = 'repo.commit.comment.create'
494
494
495 if not is_draft:
495 if not is_draft:
496 comment_data = comment.get_api_data()
496 comment_data = comment.get_api_data()
497
497
498 self._log_audit_action(
498 self._log_audit_action(
499 action, {'data': comment_data}, auth_user, comment)
499 action, {'data': comment_data}, auth_user, comment)
500
500
501 return comment
501 return comment
502
502
503 def edit(self, comment_id, text, auth_user, version):
503 def edit(self, comment_id, text, auth_user, version):
504 """
504 """
505 Change existing comment for commit or pull request.
505 Change existing comment for commit or pull request.
506
506
507 :param comment_id:
507 :param comment_id:
508 :param text:
508 :param text:
509 :param auth_user: current authenticated user calling this method
509 :param auth_user: current authenticated user calling this method
510 :param version: last comment version
510 :param version: last comment version
511 """
511 """
512 if not text:
512 if not text:
513 log.warning('Missing text for comment, skipping...')
513 log.warning('Missing text for comment, skipping...')
514 return
514 return
515
515
516 comment = ChangesetComment.get(comment_id)
516 comment = ChangesetComment.get(comment_id)
517 old_comment_text = comment.text
517 old_comment_text = comment.text
518 comment.text = text
518 comment.text = text
519 comment.modified_at = datetime.datetime.now()
519 comment.modified_at = datetime.datetime.now()
520 version = safe_int(version)
520 version = safe_int(version)
521
521
522 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
522 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
523 # would return 3 here
523 # would return 3 here
524 comment_version = ChangesetCommentHistory.get_version(comment_id)
524 comment_version = ChangesetCommentHistory.get_version(comment_id)
525
525
526 if isinstance(version, int) and (comment_version - version) != 1:
526 if isinstance(version, int) and (comment_version - version) != 1:
527 log.warning(
527 log.warning(
528 'Version mismatch comment_version {} submitted {}, skipping'.format(
528 'Version mismatch comment_version {} submitted {}, skipping'.format(
529 comment_version-1, # -1 since note above
529 comment_version-1, # -1 since note above
530 version
530 version
531 )
531 )
532 )
532 )
533 raise CommentVersionMismatch()
533 raise CommentVersionMismatch()
534
534
535 comment_history = ChangesetCommentHistory()
535 comment_history = ChangesetCommentHistory()
536 comment_history.comment_id = comment_id
536 comment_history.comment_id = comment_id
537 comment_history.version = comment_version
537 comment_history.version = comment_version
538 comment_history.created_by_user_id = auth_user.user_id
538 comment_history.created_by_user_id = auth_user.user_id
539 comment_history.text = old_comment_text
539 comment_history.text = old_comment_text
540 # TODO add email notification
540 # TODO add email notification
541 Session().add(comment_history)
541 Session().add(comment_history)
542 Session().add(comment)
542 Session().add(comment)
543 Session().flush()
543 Session().flush()
544
544
545 if comment.pull_request:
545 if comment.pull_request:
546 action = 'repo.pull_request.comment.edit'
546 action = 'repo.pull_request.comment.edit'
547 else:
547 else:
548 action = 'repo.commit.comment.edit'
548 action = 'repo.commit.comment.edit'
549
549
550 comment_data = comment.get_api_data()
550 comment_data = comment.get_api_data()
551 comment_data['old_comment_text'] = old_comment_text
551 comment_data['old_comment_text'] = old_comment_text
552 self._log_audit_action(
552 self._log_audit_action(
553 action, {'data': comment_data}, auth_user, comment)
553 action, {'data': comment_data}, auth_user, comment)
554
554
555 return comment_history
555 return comment_history
556
556
557 def delete(self, comment, auth_user):
557 def delete(self, comment, auth_user):
558 """
558 """
559 Deletes given comment
559 Deletes given comment
560 """
560 """
561 comment = self.__get_commit_comment(comment)
561 comment = self.__get_commit_comment(comment)
562 old_data = comment.get_api_data()
562 old_data = comment.get_api_data()
563 Session().delete(comment)
563 Session().delete(comment)
564
564
565 if comment.pull_request:
565 if comment.pull_request:
566 action = 'repo.pull_request.comment.delete'
566 action = 'repo.pull_request.comment.delete'
567 else:
567 else:
568 action = 'repo.commit.comment.delete'
568 action = 'repo.commit.comment.delete'
569
569
570 self._log_audit_action(
570 self._log_audit_action(
571 action, {'old_data': old_data}, auth_user, comment)
571 action, {'old_data': old_data}, auth_user, comment)
572
572
573 return comment
573 return comment
574
574
575 def get_all_comments(self, repo_id, revision=None, pull_request=None,
575 def get_all_comments(self, repo_id, revision=None, pull_request=None,
576 include_drafts=True, count_only=False):
576 include_drafts=True, count_only=False):
577 q = ChangesetComment.query()\
577 q = ChangesetComment.query()\
578 .filter(ChangesetComment.repo_id == repo_id)
578 .filter(ChangesetComment.repo_id == repo_id)
579 if revision:
579 if revision:
580 q = q.filter(ChangesetComment.revision == revision)
580 q = q.filter(ChangesetComment.revision == revision)
581 elif pull_request:
581 elif pull_request:
582 pull_request = self.__get_pull_request(pull_request)
582 pull_request = self.__get_pull_request(pull_request)
583 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
583 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
584 else:
584 else:
585 raise Exception('Please specify commit or pull_request')
585 raise Exception('Please specify commit or pull_request')
586 if not include_drafts:
586 if not include_drafts:
587 q = q.filter(ChangesetComment.draft == false())
587 q = q.filter(ChangesetComment.draft == false())
588 q = q.order_by(ChangesetComment.created_on)
588 q = q.order_by(ChangesetComment.created_on)
589 if count_only:
589 if count_only:
590 return q.count()
590 return q.count()
591
591
592 return q.all()
592 return q.all()
593
593
594 def get_url(self, comment, request=None, permalink=False, anchor=None):
594 def get_url(self, comment, request=None, permalink=False, anchor=None):
595 if not request:
595 if not request:
596 request = get_current_request()
596 request = get_current_request()
597
597
598 comment = self.__get_commit_comment(comment)
598 comment = self.__get_commit_comment(comment)
599 if anchor is None:
599 if anchor is None:
600 anchor = 'comment-{}'.format(comment.comment_id)
600 anchor = 'comment-{}'.format(comment.comment_id)
601
601
602 if comment.pull_request:
602 if comment.pull_request:
603 pull_request = comment.pull_request
603 pull_request = comment.pull_request
604 if permalink:
604 if permalink:
605 return request.route_url(
605 return request.route_url(
606 'pull_requests_global',
606 'pull_requests_global',
607 pull_request_id=pull_request.pull_request_id,
607 pull_request_id=pull_request.pull_request_id,
608 _anchor=anchor)
608 _anchor=anchor)
609 else:
609 else:
610 return request.route_url(
610 return request.route_url(
611 'pullrequest_show',
611 'pullrequest_show',
612 repo_name=safe_str(pull_request.target_repo.repo_name),
612 repo_name=safe_str(pull_request.target_repo.repo_name),
613 pull_request_id=pull_request.pull_request_id,
613 pull_request_id=pull_request.pull_request_id,
614 _anchor=anchor)
614 _anchor=anchor)
615
615
616 else:
616 else:
617 repo = comment.repo
617 repo = comment.repo
618 commit_id = comment.revision
618 commit_id = comment.revision
619
619
620 if permalink:
620 if permalink:
621 return request.route_url(
621 return request.route_url(
622 'repo_commit', repo_name=safe_str(repo.repo_id),
622 'repo_commit', repo_name=safe_str(repo.repo_id),
623 commit_id=commit_id,
623 commit_id=commit_id,
624 _anchor=anchor)
624 _anchor=anchor)
625
625
626 else:
626 else:
627 return request.route_url(
627 return request.route_url(
628 'repo_commit', repo_name=safe_str(repo.repo_name),
628 'repo_commit', repo_name=safe_str(repo.repo_name),
629 commit_id=commit_id,
629 commit_id=commit_id,
630 _anchor=anchor)
630 _anchor=anchor)
631
631
632 def get_comments(self, repo_id, revision=None, pull_request=None):
632 def get_comments(self, repo_id, revision=None, pull_request=None):
633 """
633 """
634 Gets main comments based on revision or pull_request_id
634 Gets main comments based on revision or pull_request_id
635
635
636 :param repo_id:
636 :param repo_id:
637 :param revision:
637 :param revision:
638 :param pull_request:
638 :param pull_request:
639 """
639 """
640
640
641 q = ChangesetComment.query()\
641 q = ChangesetComment.query()\
642 .filter(ChangesetComment.repo_id == repo_id)\
642 .filter(ChangesetComment.repo_id == repo_id)\
643 .filter(ChangesetComment.line_no == None)\
643 .filter(ChangesetComment.line_no == None)\
644 .filter(ChangesetComment.f_path == None)
644 .filter(ChangesetComment.f_path == None)
645 if revision:
645 if revision:
646 q = q.filter(ChangesetComment.revision == revision)
646 q = q.filter(ChangesetComment.revision == revision)
647 elif pull_request:
647 elif pull_request:
648 pull_request = self.__get_pull_request(pull_request)
648 pull_request = self.__get_pull_request(pull_request)
649 q = q.filter(ChangesetComment.pull_request == pull_request)
649 q = q.filter(ChangesetComment.pull_request == pull_request)
650 else:
650 else:
651 raise Exception('Please specify commit or pull_request')
651 raise Exception('Please specify commit or pull_request')
652 q = q.order_by(ChangesetComment.created_on)
652 q = q.order_by(ChangesetComment.created_on)
653 return q.all()
653 return q.all()
654
654
655 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
655 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
656 q = self._get_inline_comments_query(repo_id, revision, pull_request)
656 q = self._get_inline_comments_query(repo_id, revision, pull_request)
657 return self._group_comments_by_path_and_line_number(q)
657 return self._group_comments_by_path_and_line_number(q)
658
658
659 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
659 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
660 version=None):
660 version=None):
661 inline_comms = []
661 inline_comms = []
662 for fname, per_line_comments in inline_comments.items():
662 for fname, per_line_comments in inline_comments.items():
663 for lno, comments in per_line_comments.items():
663 for lno, comments in per_line_comments.items():
664 for comm in comments:
664 for comm in comments:
665 if not comm.outdated_at_version(version) and skip_outdated:
665 if not comm.outdated_at_version(version) and skip_outdated:
666 inline_comms.append(comm)
666 inline_comms.append(comm)
667
667
668 return inline_comms
668 return inline_comms
669
669
670 def get_outdated_comments(self, repo_id, pull_request):
670 def get_outdated_comments(self, repo_id, pull_request):
671 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
671 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
672 # of a pull request.
672 # of a pull request.
673 q = self._all_inline_comments_of_pull_request(pull_request)
673 q = self._all_inline_comments_of_pull_request(pull_request)
674 q = q.filter(
674 q = q.filter(
675 ChangesetComment.display_state ==
675 ChangesetComment.display_state ==
676 ChangesetComment.COMMENT_OUTDATED
676 ChangesetComment.COMMENT_OUTDATED
677 ).order_by(ChangesetComment.comment_id.asc())
677 ).order_by(ChangesetComment.comment_id.asc())
678
678
679 return self._group_comments_by_path_and_line_number(q)
679 return self._group_comments_by_path_and_line_number(q)
680
680
681 def _get_inline_comments_query(self, repo_id, revision, pull_request):
681 def _get_inline_comments_query(self, repo_id, revision, pull_request):
682 # TODO: johbo: Split this into two methods: One for PR and one for
682 # TODO: johbo: Split this into two methods: One for PR and one for
683 # commit.
683 # commit.
684 if revision:
684 if revision:
685 q = Session().query(ChangesetComment).filter(
685 q = Session().query(ChangesetComment).filter(
686 ChangesetComment.repo_id == repo_id,
686 ChangesetComment.repo_id == repo_id,
687 ChangesetComment.line_no != null(),
687 ChangesetComment.line_no != null(),
688 ChangesetComment.f_path != null(),
688 ChangesetComment.f_path != null(),
689 ChangesetComment.revision == revision)
689 ChangesetComment.revision == revision)
690
690
691 elif pull_request:
691 elif pull_request:
692 pull_request = self.__get_pull_request(pull_request)
692 pull_request = self.__get_pull_request(pull_request)
693 if not CommentsModel.use_outdated_comments(pull_request):
693 if not CommentsModel.use_outdated_comments(pull_request):
694 q = self._visible_inline_comments_of_pull_request(pull_request)
694 q = self._visible_inline_comments_of_pull_request(pull_request)
695 else:
695 else:
696 q = self._all_inline_comments_of_pull_request(pull_request)
696 q = self._all_inline_comments_of_pull_request(pull_request)
697
697
698 else:
698 else:
699 raise Exception('Please specify commit or pull_request_id')
699 raise Exception('Please specify commit or pull_request_id')
700 q = q.order_by(ChangesetComment.comment_id.asc())
700 q = q.order_by(ChangesetComment.comment_id.asc())
701 return q
701 return q
702
702
703 def _group_comments_by_path_and_line_number(self, q):
703 def _group_comments_by_path_and_line_number(self, q):
704 comments = q.all()
704 comments = q.all()
705 paths = collections.defaultdict(lambda: collections.defaultdict(list))
705 paths = collections.defaultdict(lambda: collections.defaultdict(list))
706 for co in comments:
706 for co in comments:
707 paths[co.f_path][co.line_no].append(co)
707 paths[co.f_path][co.line_no].append(co)
708 return paths
708 return paths
709
709
710 @classmethod
710 @classmethod
711 def needed_extra_diff_context(cls):
711 def needed_extra_diff_context(cls):
712 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
712 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
713
713
714 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
714 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
715 if not CommentsModel.use_outdated_comments(pull_request):
715 if not CommentsModel.use_outdated_comments(pull_request):
716 return
716 return
717
717
718 comments = self._visible_inline_comments_of_pull_request(pull_request)
718 comments = self._visible_inline_comments_of_pull_request(pull_request)
719 comments_to_outdate = comments.all()
719 comments_to_outdate = comments.all()
720
720
721 for comment in comments_to_outdate:
721 for comment in comments_to_outdate:
722 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
722 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
723
723
724 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
724 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
725 diff_line = _parse_comment_line_number(comment.line_no)
725 diff_line = _parse_comment_line_number(comment.line_no)
726
726
727 try:
727 try:
728 old_context = old_diff_proc.get_context_of_line(
728 old_context = old_diff_proc.get_context_of_line(
729 path=comment.f_path, diff_line=diff_line)
729 path=comment.f_path, diff_line=diff_line)
730 new_context = new_diff_proc.get_context_of_line(
730 new_context = new_diff_proc.get_context_of_line(
731 path=comment.f_path, diff_line=diff_line)
731 path=comment.f_path, diff_line=diff_line)
732 except (diffs.LineNotInDiffException,
732 except (diffs.LineNotInDiffException,
733 diffs.FileNotInDiffException):
733 diffs.FileNotInDiffException):
734 if not comment.draft:
734 if not comment.draft:
735 comment.display_state = ChangesetComment.COMMENT_OUTDATED
735 comment.display_state = ChangesetComment.COMMENT_OUTDATED
736 return
736 return
737
737
738 if old_context == new_context:
738 if old_context == new_context:
739 return
739 return
740
740
741 if self._should_relocate_diff_line(diff_line):
741 if self._should_relocate_diff_line(diff_line):
742 new_diff_lines = new_diff_proc.find_context(
742 new_diff_lines = new_diff_proc.find_context(
743 path=comment.f_path, context=old_context,
743 path=comment.f_path, context=old_context,
744 offset=self.DIFF_CONTEXT_BEFORE)
744 offset=self.DIFF_CONTEXT_BEFORE)
745 if not new_diff_lines and not comment.draft:
745 if not new_diff_lines and not comment.draft:
746 comment.display_state = ChangesetComment.COMMENT_OUTDATED
746 comment.display_state = ChangesetComment.COMMENT_OUTDATED
747 else:
747 else:
748 new_diff_line = self._choose_closest_diff_line(
748 new_diff_line = self._choose_closest_diff_line(
749 diff_line, new_diff_lines)
749 diff_line, new_diff_lines)
750 comment.line_no = _diff_to_comment_line_number(new_diff_line)
750 comment.line_no = _diff_to_comment_line_number(new_diff_line)
751 else:
751 else:
752 if not comment.draft:
752 if not comment.draft:
753 comment.display_state = ChangesetComment.COMMENT_OUTDATED
753 comment.display_state = ChangesetComment.COMMENT_OUTDATED
754
754
755 def _should_relocate_diff_line(self, diff_line):
755 def _should_relocate_diff_line(self, diff_line):
756 """
756 """
757 Checks if relocation shall be tried for the given `diff_line`.
757 Checks if relocation shall be tried for the given `diff_line`.
758
758
759 If a comment points into the first lines, then we can have a situation
759 If a comment points into the first lines, then we can have a situation
760 that after an update another line has been added on top. In this case
760 that after an update another line has been added on top. In this case
761 we would find the context still and move the comment around. This
761 we would find the context still and move the comment around. This
762 would be wrong.
762 would be wrong.
763 """
763 """
764 should_relocate = (
764 should_relocate = (
765 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
765 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
766 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
766 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
767 return should_relocate
767 return should_relocate
768
768
769 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
769 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
770 candidate = new_diff_lines[0]
770 candidate = new_diff_lines[0]
771 best_delta = _diff_line_delta(diff_line, candidate)
771 best_delta = _diff_line_delta(diff_line, candidate)
772 for new_diff_line in new_diff_lines[1:]:
772 for new_diff_line in new_diff_lines[1:]:
773 delta = _diff_line_delta(diff_line, new_diff_line)
773 delta = _diff_line_delta(diff_line, new_diff_line)
774 if delta < best_delta:
774 if delta < best_delta:
775 candidate = new_diff_line
775 candidate = new_diff_line
776 best_delta = delta
776 best_delta = delta
777 return candidate
777 return candidate
778
778
779 def _visible_inline_comments_of_pull_request(self, pull_request):
779 def _visible_inline_comments_of_pull_request(self, pull_request):
780 comments = self._all_inline_comments_of_pull_request(pull_request)
780 comments = self._all_inline_comments_of_pull_request(pull_request)
781 comments = comments.filter(
781 comments = comments.filter(
782 coalesce(ChangesetComment.display_state, '') !=
782 coalesce(ChangesetComment.display_state, '') !=
783 ChangesetComment.COMMENT_OUTDATED)
783 ChangesetComment.COMMENT_OUTDATED)
784 return comments
784 return comments
785
785
786 def _all_inline_comments_of_pull_request(self, pull_request):
786 def _all_inline_comments_of_pull_request(self, pull_request):
787 comments = Session().query(ChangesetComment)\
787 comments = Session().query(ChangesetComment)\
788 .filter(ChangesetComment.line_no != None)\
788 .filter(ChangesetComment.line_no != None)\
789 .filter(ChangesetComment.f_path != None)\
789 .filter(ChangesetComment.f_path != None)\
790 .filter(ChangesetComment.pull_request == pull_request)
790 .filter(ChangesetComment.pull_request == pull_request)
791 return comments
791 return comments
792
792
793 def _all_general_comments_of_pull_request(self, pull_request):
793 def _all_general_comments_of_pull_request(self, pull_request):
794 comments = Session().query(ChangesetComment)\
794 comments = Session().query(ChangesetComment)\
795 .filter(ChangesetComment.line_no == None)\
795 .filter(ChangesetComment.line_no == None)\
796 .filter(ChangesetComment.f_path == None)\
796 .filter(ChangesetComment.f_path == None)\
797 .filter(ChangesetComment.pull_request == pull_request)
797 .filter(ChangesetComment.pull_request == pull_request)
798
798
799 return comments
799 return comments
800
800
801 @staticmethod
801 @staticmethod
802 def use_outdated_comments(pull_request):
802 def use_outdated_comments(pull_request):
803 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
803 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
804 settings = settings_model.get_general_settings()
804 settings = settings_model.get_general_settings()
805 return settings.get('rhodecode_use_outdated_comments', False)
805 return settings.get('rhodecode_use_outdated_comments', False)
806
806
807 def trigger_commit_comment_hook(self, repo, user, action, data=None):
807 def trigger_commit_comment_hook(self, repo, user, action, data=None):
808 repo = self._get_repo(repo)
808 repo = self._get_repo(repo)
809 target_scm = repo.scm_instance()
809 target_scm = repo.scm_instance()
810 if action == 'create':
810 if action == 'create':
811 trigger_hook = hooks_utils.trigger_comment_commit_hooks
811 trigger_hook = hooks_utils.trigger_comment_commit_hooks
812 elif action == 'edit':
812 elif action == 'edit':
813 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
813 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
814 else:
814 else:
815 return
815 return
816
816
817 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
817 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
818 repo, action, trigger_hook)
818 repo, action, trigger_hook)
819 trigger_hook(
819 trigger_hook(
820 username=user.username,
820 username=user.username,
821 repo_name=repo.repo_name,
821 repo_name=repo.repo_name,
822 repo_type=target_scm.alias,
822 repo_type=target_scm.alias,
823 repo=repo,
823 repo=repo,
824 data=data)
824 data=data)
825
825
826
826
827 def _parse_comment_line_number(line_no):
827 def _parse_comment_line_number(line_no):
828 """
828 """
829 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
829 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
830 """
830 """
831 old_line = None
831 old_line = None
832 new_line = None
832 new_line = None
833 if line_no.startswith('o'):
833 if line_no.startswith('o'):
834 old_line = int(line_no[1:])
834 old_line = int(line_no[1:])
835 elif line_no.startswith('n'):
835 elif line_no.startswith('n'):
836 new_line = int(line_no[1:])
836 new_line = int(line_no[1:])
837 else:
837 else:
838 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
838 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
839 return diffs.DiffLineNumber(old_line, new_line)
839 return diffs.DiffLineNumber(old_line, new_line)
840
840
841
841
842 def _diff_to_comment_line_number(diff_line):
842 def _diff_to_comment_line_number(diff_line):
843 if diff_line.new is not None:
843 if diff_line.new is not None:
844 return u'n{}'.format(diff_line.new)
844 return u'n{}'.format(diff_line.new)
845 elif diff_line.old is not None:
845 elif diff_line.old is not None:
846 return u'o{}'.format(diff_line.old)
846 return u'o{}'.format(diff_line.old)
847 return u''
847 return u''
848
848
849
849
850 def _diff_line_delta(a, b):
850 def _diff_line_delta(a, b):
851 if None not in (a.new, b.new):
851 if None not in (a.new, b.new):
852 return abs(a.new - b.new)
852 return abs(a.new - b.new)
853 elif None not in (a.old, b.old):
853 elif None not in (a.old, b.old):
854 return abs(a.old - b.old)
854 return abs(a.old - b.old)
855 else:
855 else:
856 raise ValueError(
856 raise ValueError(
857 "Cannot compute delta between {} and {}".format(a, b))
857 "Cannot compute delta between {} and {}".format(a, b))
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,641 +1,640 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 """
20 """
22 this is forms validation classes
21 this is forms validation classes
23 http://formencode.org/module-formencode.validators.html
22 http://formencode.org/module-formencode.validators.html
24 for list off all availible validators
23 for list off all availible validators
25
24
26 we can create our own validators
25 we can create our own validators
27
26
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
27 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 pre_validators [] These validators will be applied before the schema
28 pre_validators [] These validators will be applied before the schema
30 chained_validators [] These validators will be applied after the schema
29 chained_validators [] These validators will be applied after the schema
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
30 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
31 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
32 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
33 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35
34
36
35
37 <name> = formencode.validators.<name of validator>
36 <name> = formencode.validators.<name of validator>
38 <name> must equal form name
37 <name> must equal form name
39 list=[1,2,3,4,5]
38 list=[1,2,3,4,5]
40 for SELECT use formencode.All(OneOf(list), Int())
39 for SELECT use formencode.All(OneOf(list), Int())
41
40
42 """
41 """
43
42
44 import deform
43 import deform
45 import logging
44 import logging
46 import formencode
45 import formencode
47
46
48 from pkg_resources import resource_filename
47 from pkg_resources import resource_filename
49 from formencode import All, Pipe
48 from formencode import All, Pipe
50
49
51 from pyramid.threadlocal import get_current_request
50 from pyramid.threadlocal import get_current_request
52
51
53 from rhodecode import BACKENDS
52 from rhodecode import BACKENDS
54 from rhodecode.lib import helpers
53 from rhodecode.lib import helpers
55 from rhodecode.model import validators as v
54 from rhodecode.model import validators as v
56
55
57 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
58
57
59
58
60 deform_templates = resource_filename('deform', 'templates')
59 deform_templates = resource_filename('deform', 'templates')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
60 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 search_path = (rhodecode_templates, deform_templates)
61 search_path = (rhodecode_templates, deform_templates)
63
62
64
63
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
64 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
65 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 def __call__(self, template_name, **kw):
66 def __call__(self, template_name, **kw):
68 kw['h'] = helpers
67 kw['h'] = helpers
69 kw['request'] = get_current_request()
68 kw['request'] = get_current_request()
70 return self.load(template_name)(**kw)
69 return self.load(template_name)(**kw)
71
70
72
71
73 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
74 deform.Form.set_default_renderer(form_renderer)
73 deform.Form.set_default_renderer(form_renderer)
75
74
76
75
77 def LoginForm(localizer):
76 def LoginForm(localizer):
78 _ = localizer
77 _ = localizer
79
78
80 class _LoginForm(formencode.Schema):
79 class _LoginForm(formencode.Schema):
81 allow_extra_fields = True
80 allow_extra_fields = True
82 filter_extra_fields = True
81 filter_extra_fields = True
83 username = v.UnicodeString(
82 username = v.UnicodeString(
84 strip=True,
83 strip=True,
85 min=1,
84 min=1,
86 not_empty=True,
85 not_empty=True,
87 messages={
86 messages={
88 'empty': _(u'Please enter a login'),
87 'empty': _(u'Please enter a login'),
89 'tooShort': _(u'Enter a value %(min)i characters long or more')
88 'tooShort': _(u'Enter a value %(min)i characters long or more')
90 }
89 }
91 )
90 )
92
91
93 password = v.UnicodeString(
92 password = v.UnicodeString(
94 strip=False,
93 strip=False,
95 min=3,
94 min=3,
96 max=72,
95 max=72,
97 not_empty=True,
96 not_empty=True,
98 messages={
97 messages={
99 'empty': _(u'Please enter a password'),
98 'empty': _(u'Please enter a password'),
100 'tooShort': _(u'Enter %(min)i characters or more')}
99 'tooShort': _(u'Enter %(min)i characters or more')}
101 )
100 )
102
101
103 remember = v.StringBoolean(if_missing=False)
102 remember = v.StringBoolean(if_missing=False)
104
103
105 chained_validators = [v.ValidAuth(localizer)]
104 chained_validators = [v.ValidAuth(localizer)]
106 return _LoginForm
105 return _LoginForm
107
106
108
107
109 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
108 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
110 old_data = old_data or {}
109 old_data = old_data or {}
111 available_languages = available_languages or []
110 available_languages = available_languages or []
112 _ = localizer
111 _ = localizer
113
112
114 class _UserForm(formencode.Schema):
113 class _UserForm(formencode.Schema):
115 allow_extra_fields = True
114 allow_extra_fields = True
116 filter_extra_fields = True
115 filter_extra_fields = True
117 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
116 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
118 v.ValidUsername(localizer, edit, old_data))
117 v.ValidUsername(localizer, edit, old_data))
119 if edit:
118 if edit:
120 new_password = All(
119 new_password = All(
121 v.ValidPassword(localizer),
120 v.ValidPassword(localizer),
122 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
121 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
123 )
122 )
124 password_confirmation = All(
123 password_confirmation = All(
125 v.ValidPassword(localizer),
124 v.ValidPassword(localizer),
126 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
125 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
127 )
126 )
128 admin = v.StringBoolean(if_missing=False)
127 admin = v.StringBoolean(if_missing=False)
129 else:
128 else:
130 password = All(
129 password = All(
131 v.ValidPassword(localizer),
130 v.ValidPassword(localizer),
132 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
131 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
133 )
132 )
134 password_confirmation = All(
133 password_confirmation = All(
135 v.ValidPassword(localizer),
134 v.ValidPassword(localizer),
136 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
135 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
137 )
136 )
138
137
139 password_change = v.StringBoolean(if_missing=False)
138 password_change = v.StringBoolean(if_missing=False)
140 create_repo_group = v.StringBoolean(if_missing=False)
139 create_repo_group = v.StringBoolean(if_missing=False)
141
140
142 active = v.StringBoolean(if_missing=False)
141 active = v.StringBoolean(if_missing=False)
143 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
142 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
144 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
143 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
145 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
144 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
146 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
145 description = v.UnicodeString(strip=True, min=1, max=250, not_empty=False,
147 if_missing='')
146 if_missing='')
148 extern_name = v.UnicodeString(strip=True)
147 extern_name = v.UnicodeString(strip=True)
149 extern_type = v.UnicodeString(strip=True)
148 extern_type = v.UnicodeString(strip=True)
150 language = v.OneOf(available_languages, hideList=False,
149 language = v.OneOf(available_languages, hideList=False,
151 testValueList=True, if_missing=None)
150 testValueList=True, if_missing=None)
152 chained_validators = [v.ValidPasswordsMatch(localizer)]
151 chained_validators = [v.ValidPasswordsMatch(localizer)]
153 return _UserForm
152 return _UserForm
154
153
155
154
156 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
155 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
157 old_data = old_data or {}
156 old_data = old_data or {}
158 _ = localizer
157 _ = localizer
159
158
160 class _UserGroupForm(formencode.Schema):
159 class _UserGroupForm(formencode.Schema):
161 allow_extra_fields = True
160 allow_extra_fields = True
162 filter_extra_fields = True
161 filter_extra_fields = True
163
162
164 users_group_name = All(
163 users_group_name = All(
165 v.UnicodeString(strip=True, min=1, not_empty=True),
164 v.UnicodeString(strip=True, min=1, not_empty=True),
166 v.ValidUserGroup(localizer, edit, old_data)
165 v.ValidUserGroup(localizer, edit, old_data)
167 )
166 )
168 user_group_description = v.UnicodeString(strip=True, min=1,
167 user_group_description = v.UnicodeString(strip=True, min=1,
169 not_empty=False)
168 not_empty=False)
170
169
171 users_group_active = v.StringBoolean(if_missing=False)
170 users_group_active = v.StringBoolean(if_missing=False)
172
171
173 if edit:
172 if edit:
174 # this is user group owner
173 # this is user group owner
175 user = All(
174 user = All(
176 v.UnicodeString(not_empty=True),
175 v.UnicodeString(not_empty=True),
177 v.ValidRepoUser(localizer, allow_disabled))
176 v.ValidRepoUser(localizer, allow_disabled))
178 return _UserGroupForm
177 return _UserGroupForm
179
178
180
179
181 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
180 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
182 can_create_in_root=False, allow_disabled=False):
181 can_create_in_root=False, allow_disabled=False):
183 _ = localizer
182 _ = localizer
184 old_data = old_data or {}
183 old_data = old_data or {}
185 available_groups = available_groups or []
184 available_groups = available_groups or []
186
185
187 class _RepoGroupForm(formencode.Schema):
186 class _RepoGroupForm(formencode.Schema):
188 allow_extra_fields = True
187 allow_extra_fields = True
189 filter_extra_fields = False
188 filter_extra_fields = False
190
189
191 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
190 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
192 v.SlugifyName(localizer),)
191 v.SlugifyName(localizer),)
193 group_description = v.UnicodeString(strip=True, min=1,
192 group_description = v.UnicodeString(strip=True, min=1,
194 not_empty=False)
193 not_empty=False)
195 group_copy_permissions = v.StringBoolean(if_missing=False)
194 group_copy_permissions = v.StringBoolean(if_missing=False)
196
195
197 group_parent_id = v.OneOf(available_groups, hideList=False,
196 group_parent_id = v.OneOf(available_groups, hideList=False,
198 testValueList=True, not_empty=True)
197 testValueList=True, not_empty=True)
199 enable_locking = v.StringBoolean(if_missing=False)
198 enable_locking = v.StringBoolean(if_missing=False)
200 chained_validators = [
199 chained_validators = [
201 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
200 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
202
201
203 if edit:
202 if edit:
204 # this is repo group owner
203 # this is repo group owner
205 user = All(
204 user = All(
206 v.UnicodeString(not_empty=True),
205 v.UnicodeString(not_empty=True),
207 v.ValidRepoUser(localizer, allow_disabled))
206 v.ValidRepoUser(localizer, allow_disabled))
208 return _RepoGroupForm
207 return _RepoGroupForm
209
208
210
209
211 def RegisterForm(localizer, edit=False, old_data=None):
210 def RegisterForm(localizer, edit=False, old_data=None):
212 _ = localizer
211 _ = localizer
213 old_data = old_data or {}
212 old_data = old_data or {}
214
213
215 class _RegisterForm(formencode.Schema):
214 class _RegisterForm(formencode.Schema):
216 allow_extra_fields = True
215 allow_extra_fields = True
217 filter_extra_fields = True
216 filter_extra_fields = True
218 username = All(
217 username = All(
219 v.ValidUsername(localizer, edit, old_data),
218 v.ValidUsername(localizer, edit, old_data),
220 v.UnicodeString(strip=True, min=1, not_empty=True)
219 v.UnicodeString(strip=True, min=1, not_empty=True)
221 )
220 )
222 password = All(
221 password = All(
223 v.ValidPassword(localizer),
222 v.ValidPassword(localizer),
224 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
223 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
225 )
224 )
226 password_confirmation = All(
225 password_confirmation = All(
227 v.ValidPassword(localizer),
226 v.ValidPassword(localizer),
228 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
227 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
229 )
228 )
230 active = v.StringBoolean(if_missing=False)
229 active = v.StringBoolean(if_missing=False)
231 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
232 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
231 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
233 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
232 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
234
233
235 chained_validators = [v.ValidPasswordsMatch(localizer)]
234 chained_validators = [v.ValidPasswordsMatch(localizer)]
236 return _RegisterForm
235 return _RegisterForm
237
236
238
237
239 def PasswordResetForm(localizer):
238 def PasswordResetForm(localizer):
240 _ = localizer
239 _ = localizer
241
240
242 class _PasswordResetForm(formencode.Schema):
241 class _PasswordResetForm(formencode.Schema):
243 allow_extra_fields = True
242 allow_extra_fields = True
244 filter_extra_fields = True
243 filter_extra_fields = True
245 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
244 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
246 return _PasswordResetForm
245 return _PasswordResetForm
247
246
248
247
249 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
248 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
250 _ = localizer
249 _ = localizer
251 old_data = old_data or {}
250 old_data = old_data or {}
252 repo_groups = repo_groups or []
251 repo_groups = repo_groups or []
253 supported_backends = BACKENDS.keys()
252 supported_backends = BACKENDS.keys()
254
253
255 class _RepoForm(formencode.Schema):
254 class _RepoForm(formencode.Schema):
256 allow_extra_fields = True
255 allow_extra_fields = True
257 filter_extra_fields = False
256 filter_extra_fields = False
258 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
257 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
259 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
258 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
260 repo_group = All(v.CanWriteGroup(localizer, old_data),
259 repo_group = All(v.CanWriteGroup(localizer, old_data),
261 v.OneOf(repo_groups, hideList=True))
260 v.OneOf(repo_groups, hideList=True))
262 repo_type = v.OneOf(supported_backends, required=False,
261 repo_type = v.OneOf(supported_backends, required=False,
263 if_missing=old_data.get('repo_type'))
262 if_missing=old_data.get('repo_type'))
264 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
263 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
265 repo_private = v.StringBoolean(if_missing=False)
264 repo_private = v.StringBoolean(if_missing=False)
266 repo_copy_permissions = v.StringBoolean(if_missing=False)
265 repo_copy_permissions = v.StringBoolean(if_missing=False)
267 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
266 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
268
267
269 repo_enable_statistics = v.StringBoolean(if_missing=False)
268 repo_enable_statistics = v.StringBoolean(if_missing=False)
270 repo_enable_downloads = v.StringBoolean(if_missing=False)
269 repo_enable_downloads = v.StringBoolean(if_missing=False)
271 repo_enable_locking = v.StringBoolean(if_missing=False)
270 repo_enable_locking = v.StringBoolean(if_missing=False)
272
271
273 if edit:
272 if edit:
274 # this is repo owner
273 # this is repo owner
275 user = All(
274 user = All(
276 v.UnicodeString(not_empty=True),
275 v.UnicodeString(not_empty=True),
277 v.ValidRepoUser(localizer, allow_disabled))
276 v.ValidRepoUser(localizer, allow_disabled))
278 clone_uri_change = v.UnicodeString(
277 clone_uri_change = v.UnicodeString(
279 not_empty=False, if_missing=v.Missing)
278 not_empty=False, if_missing=v.Missing)
280
279
281 chained_validators = [v.ValidCloneUri(localizer),
280 chained_validators = [v.ValidCloneUri(localizer),
282 v.ValidRepoName(localizer, edit, old_data)]
281 v.ValidRepoName(localizer, edit, old_data)]
283 return _RepoForm
282 return _RepoForm
284
283
285
284
286 def RepoPermsForm(localizer):
285 def RepoPermsForm(localizer):
287 _ = localizer
286 _ = localizer
288
287
289 class _RepoPermsForm(formencode.Schema):
288 class _RepoPermsForm(formencode.Schema):
290 allow_extra_fields = True
289 allow_extra_fields = True
291 filter_extra_fields = False
290 filter_extra_fields = False
292 chained_validators = [v.ValidPerms(localizer, type_='repo')]
291 chained_validators = [v.ValidPerms(localizer, type_='repo')]
293 return _RepoPermsForm
292 return _RepoPermsForm
294
293
295
294
296 def RepoGroupPermsForm(localizer, valid_recursive_choices):
295 def RepoGroupPermsForm(localizer, valid_recursive_choices):
297 _ = localizer
296 _ = localizer
298
297
299 class _RepoGroupPermsForm(formencode.Schema):
298 class _RepoGroupPermsForm(formencode.Schema):
300 allow_extra_fields = True
299 allow_extra_fields = True
301 filter_extra_fields = False
300 filter_extra_fields = False
302 recursive = v.OneOf(valid_recursive_choices)
301 recursive = v.OneOf(valid_recursive_choices)
303 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
302 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
304 return _RepoGroupPermsForm
303 return _RepoGroupPermsForm
305
304
306
305
307 def UserGroupPermsForm(localizer):
306 def UserGroupPermsForm(localizer):
308 _ = localizer
307 _ = localizer
309
308
310 class _UserPermsForm(formencode.Schema):
309 class _UserPermsForm(formencode.Schema):
311 allow_extra_fields = True
310 allow_extra_fields = True
312 filter_extra_fields = False
311 filter_extra_fields = False
313 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
312 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
314 return _UserPermsForm
313 return _UserPermsForm
315
314
316
315
317 def RepoFieldForm(localizer):
316 def RepoFieldForm(localizer):
318 _ = localizer
317 _ = localizer
319
318
320 class _RepoFieldForm(formencode.Schema):
319 class _RepoFieldForm(formencode.Schema):
321 filter_extra_fields = True
320 filter_extra_fields = True
322 allow_extra_fields = True
321 allow_extra_fields = True
323
322
324 new_field_key = All(v.FieldKey(localizer),
323 new_field_key = All(v.FieldKey(localizer),
325 v.UnicodeString(strip=True, min=3, not_empty=True))
324 v.UnicodeString(strip=True, min=3, not_empty=True))
326 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
325 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
327 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
326 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
328 if_missing='str')
327 if_missing='str')
329 new_field_label = v.UnicodeString(not_empty=False)
328 new_field_label = v.UnicodeString(not_empty=False)
330 new_field_desc = v.UnicodeString(not_empty=False)
329 new_field_desc = v.UnicodeString(not_empty=False)
331 return _RepoFieldForm
330 return _RepoFieldForm
332
331
333
332
334 def RepoForkForm(localizer, edit=False, old_data=None,
333 def RepoForkForm(localizer, edit=False, old_data=None,
335 supported_backends=BACKENDS.keys(), repo_groups=None):
334 supported_backends=BACKENDS.keys(), repo_groups=None):
336 _ = localizer
335 _ = localizer
337 old_data = old_data or {}
336 old_data = old_data or {}
338 repo_groups = repo_groups or []
337 repo_groups = repo_groups or []
339
338
340 class _RepoForkForm(formencode.Schema):
339 class _RepoForkForm(formencode.Schema):
341 allow_extra_fields = True
340 allow_extra_fields = True
342 filter_extra_fields = False
341 filter_extra_fields = False
343 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
342 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
344 v.SlugifyName(localizer))
343 v.SlugifyName(localizer))
345 repo_group = All(v.CanWriteGroup(localizer, ),
344 repo_group = All(v.CanWriteGroup(localizer, ),
346 v.OneOf(repo_groups, hideList=True))
345 v.OneOf(repo_groups, hideList=True))
347 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
346 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
348 description = v.UnicodeString(strip=True, min=1, not_empty=True)
347 description = v.UnicodeString(strip=True, min=1, not_empty=True)
349 private = v.StringBoolean(if_missing=False)
348 private = v.StringBoolean(if_missing=False)
350 copy_permissions = v.StringBoolean(if_missing=False)
349 copy_permissions = v.StringBoolean(if_missing=False)
351 fork_parent_id = v.UnicodeString()
350 fork_parent_id = v.UnicodeString()
352 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
351 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
353 return _RepoForkForm
352 return _RepoForkForm
354
353
355
354
356 def ApplicationSettingsForm(localizer):
355 def ApplicationSettingsForm(localizer):
357 _ = localizer
356 _ = localizer
358
357
359 class _ApplicationSettingsForm(formencode.Schema):
358 class _ApplicationSettingsForm(formencode.Schema):
360 allow_extra_fields = True
359 allow_extra_fields = True
361 filter_extra_fields = False
360 filter_extra_fields = False
362 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
361 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
363 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
362 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
364 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
363 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
365 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
364 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
366 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
365 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
367 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
366 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
368 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
367 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
369 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
368 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
370 return _ApplicationSettingsForm
369 return _ApplicationSettingsForm
371
370
372
371
373 def ApplicationVisualisationForm(localizer):
372 def ApplicationVisualisationForm(localizer):
374 from rhodecode.model.db import Repository
373 from rhodecode.model.db import Repository
375 _ = localizer
374 _ = localizer
376
375
377 class _ApplicationVisualisationForm(formencode.Schema):
376 class _ApplicationVisualisationForm(formencode.Schema):
378 allow_extra_fields = True
377 allow_extra_fields = True
379 filter_extra_fields = False
378 filter_extra_fields = False
380 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
379 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
381 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
380 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
382 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
381 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
383
382
384 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
383 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
385 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
384 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
386 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
385 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
387 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
386 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
388 rhodecode_show_version = v.StringBoolean(if_missing=False)
387 rhodecode_show_version = v.StringBoolean(if_missing=False)
389 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
388 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
390 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
389 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
391 rhodecode_gravatar_url = v.UnicodeString(min=3)
390 rhodecode_gravatar_url = v.UnicodeString(min=3)
392 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
391 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
393 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
392 rhodecode_clone_uri_id_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_ID)
394 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
393 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
395 rhodecode_support_url = v.UnicodeString()
394 rhodecode_support_url = v.UnicodeString()
396 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
395 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
397 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
396 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
398 return _ApplicationVisualisationForm
397 return _ApplicationVisualisationForm
399
398
400
399
401 class _BaseVcsSettingsForm(formencode.Schema):
400 class _BaseVcsSettingsForm(formencode.Schema):
402
401
403 allow_extra_fields = True
402 allow_extra_fields = True
404 filter_extra_fields = False
403 filter_extra_fields = False
405 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
404 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
406 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
405 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
407 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
406 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
408
407
409 # PR/Code-review
408 # PR/Code-review
410 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
409 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
411 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
410 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
412
411
413 # hg
412 # hg
414 extensions_largefiles = v.StringBoolean(if_missing=False)
413 extensions_largefiles = v.StringBoolean(if_missing=False)
415 extensions_evolve = v.StringBoolean(if_missing=False)
414 extensions_evolve = v.StringBoolean(if_missing=False)
416 phases_publish = v.StringBoolean(if_missing=False)
415 phases_publish = v.StringBoolean(if_missing=False)
417
416
418 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
417 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
419 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
418 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
420
419
421 # git
420 # git
422 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
421 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
423 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
422 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
424 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
423 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
425
424
426 # svn
425 # svn
427 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
426 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
428 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
427 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
429
428
430 # cache
429 # cache
431 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
430 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
432
431
433
432
434 def ApplicationUiSettingsForm(localizer):
433 def ApplicationUiSettingsForm(localizer):
435 _ = localizer
434 _ = localizer
436
435
437 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
436 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
438 web_push_ssl = v.StringBoolean(if_missing=False)
437 web_push_ssl = v.StringBoolean(if_missing=False)
439 paths_root_path = All(
438 paths_root_path = All(
440 v.ValidPath(localizer),
439 v.ValidPath(localizer),
441 v.UnicodeString(strip=True, min=1, not_empty=True)
440 v.UnicodeString(strip=True, min=1, not_empty=True)
442 )
441 )
443 largefiles_usercache = All(
442 largefiles_usercache = All(
444 v.ValidPath(localizer),
443 v.ValidPath(localizer),
445 v.UnicodeString(strip=True, min=2, not_empty=True))
444 v.UnicodeString(strip=True, min=2, not_empty=True))
446 vcs_git_lfs_store_location = All(
445 vcs_git_lfs_store_location = All(
447 v.ValidPath(localizer),
446 v.ValidPath(localizer),
448 v.UnicodeString(strip=True, min=2, not_empty=True))
447 v.UnicodeString(strip=True, min=2, not_empty=True))
449 extensions_hgsubversion = v.StringBoolean(if_missing=False)
448 extensions_hgsubversion = v.StringBoolean(if_missing=False)
450 extensions_hggit = v.StringBoolean(if_missing=False)
449 extensions_hggit = v.StringBoolean(if_missing=False)
451 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
450 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
452 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
451 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
453 return _ApplicationUiSettingsForm
452 return _ApplicationUiSettingsForm
454
453
455
454
456 def RepoVcsSettingsForm(localizer, repo_name):
455 def RepoVcsSettingsForm(localizer, repo_name):
457 _ = localizer
456 _ = localizer
458
457
459 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
458 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
460 inherit_global_settings = v.StringBoolean(if_missing=False)
459 inherit_global_settings = v.StringBoolean(if_missing=False)
461 new_svn_branch = v.ValidSvnPattern(localizer,
460 new_svn_branch = v.ValidSvnPattern(localizer,
462 section='vcs_svn_branch', repo_name=repo_name)
461 section='vcs_svn_branch', repo_name=repo_name)
463 new_svn_tag = v.ValidSvnPattern(localizer,
462 new_svn_tag = v.ValidSvnPattern(localizer,
464 section='vcs_svn_tag', repo_name=repo_name)
463 section='vcs_svn_tag', repo_name=repo_name)
465 return _RepoVcsSettingsForm
464 return _RepoVcsSettingsForm
466
465
467
466
468 def LabsSettingsForm(localizer):
467 def LabsSettingsForm(localizer):
469 _ = localizer
468 _ = localizer
470
469
471 class _LabSettingsForm(formencode.Schema):
470 class _LabSettingsForm(formencode.Schema):
472 allow_extra_fields = True
471 allow_extra_fields = True
473 filter_extra_fields = False
472 filter_extra_fields = False
474 return _LabSettingsForm
473 return _LabSettingsForm
475
474
476
475
477 def ApplicationPermissionsForm(
476 def ApplicationPermissionsForm(
478 localizer, register_choices, password_reset_choices,
477 localizer, register_choices, password_reset_choices,
479 extern_activate_choices):
478 extern_activate_choices):
480 _ = localizer
479 _ = localizer
481
480
482 class _DefaultPermissionsForm(formencode.Schema):
481 class _DefaultPermissionsForm(formencode.Schema):
483 allow_extra_fields = True
482 allow_extra_fields = True
484 filter_extra_fields = True
483 filter_extra_fields = True
485
484
486 anonymous = v.StringBoolean(if_missing=False)
485 anonymous = v.StringBoolean(if_missing=False)
487 default_register = v.OneOf(register_choices)
486 default_register = v.OneOf(register_choices)
488 default_register_message = v.UnicodeString()
487 default_register_message = v.UnicodeString()
489 default_password_reset = v.OneOf(password_reset_choices)
488 default_password_reset = v.OneOf(password_reset_choices)
490 default_extern_activate = v.OneOf(extern_activate_choices)
489 default_extern_activate = v.OneOf(extern_activate_choices)
491 return _DefaultPermissionsForm
490 return _DefaultPermissionsForm
492
491
493
492
494 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
493 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
495 user_group_perms_choices):
494 user_group_perms_choices):
496 _ = localizer
495 _ = localizer
497
496
498 class _ObjectPermissionsForm(formencode.Schema):
497 class _ObjectPermissionsForm(formencode.Schema):
499 allow_extra_fields = True
498 allow_extra_fields = True
500 filter_extra_fields = True
499 filter_extra_fields = True
501 overwrite_default_repo = v.StringBoolean(if_missing=False)
500 overwrite_default_repo = v.StringBoolean(if_missing=False)
502 overwrite_default_group = v.StringBoolean(if_missing=False)
501 overwrite_default_group = v.StringBoolean(if_missing=False)
503 overwrite_default_user_group = v.StringBoolean(if_missing=False)
502 overwrite_default_user_group = v.StringBoolean(if_missing=False)
504
503
505 default_repo_perm = v.OneOf(repo_perms_choices)
504 default_repo_perm = v.OneOf(repo_perms_choices)
506 default_group_perm = v.OneOf(group_perms_choices)
505 default_group_perm = v.OneOf(group_perms_choices)
507 default_user_group_perm = v.OneOf(user_group_perms_choices)
506 default_user_group_perm = v.OneOf(user_group_perms_choices)
508
507
509 return _ObjectPermissionsForm
508 return _ObjectPermissionsForm
510
509
511
510
512 def BranchPermissionsForm(localizer, branch_perms_choices):
511 def BranchPermissionsForm(localizer, branch_perms_choices):
513 _ = localizer
512 _ = localizer
514
513
515 class _BranchPermissionsForm(formencode.Schema):
514 class _BranchPermissionsForm(formencode.Schema):
516 allow_extra_fields = True
515 allow_extra_fields = True
517 filter_extra_fields = True
516 filter_extra_fields = True
518 overwrite_default_branch = v.StringBoolean(if_missing=False)
517 overwrite_default_branch = v.StringBoolean(if_missing=False)
519 default_branch_perm = v.OneOf(branch_perms_choices)
518 default_branch_perm = v.OneOf(branch_perms_choices)
520
519
521 return _BranchPermissionsForm
520 return _BranchPermissionsForm
522
521
523
522
524 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
523 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
525 repo_group_create_choices, user_group_create_choices,
524 repo_group_create_choices, user_group_create_choices,
526 fork_choices, inherit_default_permissions_choices):
525 fork_choices, inherit_default_permissions_choices):
527 _ = localizer
526 _ = localizer
528
527
529 class _DefaultPermissionsForm(formencode.Schema):
528 class _DefaultPermissionsForm(formencode.Schema):
530 allow_extra_fields = True
529 allow_extra_fields = True
531 filter_extra_fields = True
530 filter_extra_fields = True
532
531
533 anonymous = v.StringBoolean(if_missing=False)
532 anonymous = v.StringBoolean(if_missing=False)
534
533
535 default_repo_create = v.OneOf(create_choices)
534 default_repo_create = v.OneOf(create_choices)
536 default_repo_create_on_write = v.OneOf(create_on_write_choices)
535 default_repo_create_on_write = v.OneOf(create_on_write_choices)
537 default_user_group_create = v.OneOf(user_group_create_choices)
536 default_user_group_create = v.OneOf(user_group_create_choices)
538 default_repo_group_create = v.OneOf(repo_group_create_choices)
537 default_repo_group_create = v.OneOf(repo_group_create_choices)
539 default_fork_create = v.OneOf(fork_choices)
538 default_fork_create = v.OneOf(fork_choices)
540 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
539 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
541 return _DefaultPermissionsForm
540 return _DefaultPermissionsForm
542
541
543
542
544 def UserIndividualPermissionsForm(localizer):
543 def UserIndividualPermissionsForm(localizer):
545 _ = localizer
544 _ = localizer
546
545
547 class _DefaultPermissionsForm(formencode.Schema):
546 class _DefaultPermissionsForm(formencode.Schema):
548 allow_extra_fields = True
547 allow_extra_fields = True
549 filter_extra_fields = True
548 filter_extra_fields = True
550
549
551 inherit_default_permissions = v.StringBoolean(if_missing=False)
550 inherit_default_permissions = v.StringBoolean(if_missing=False)
552 return _DefaultPermissionsForm
551 return _DefaultPermissionsForm
553
552
554
553
555 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
554 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
556 _ = localizer
555 _ = localizer
557 old_data = old_data or {}
556 old_data = old_data or {}
558
557
559 class _DefaultsForm(formencode.Schema):
558 class _DefaultsForm(formencode.Schema):
560 allow_extra_fields = True
559 allow_extra_fields = True
561 filter_extra_fields = True
560 filter_extra_fields = True
562 default_repo_type = v.OneOf(supported_backends)
561 default_repo_type = v.OneOf(supported_backends)
563 default_repo_private = v.StringBoolean(if_missing=False)
562 default_repo_private = v.StringBoolean(if_missing=False)
564 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
563 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
565 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
564 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
566 default_repo_enable_locking = v.StringBoolean(if_missing=False)
565 default_repo_enable_locking = v.StringBoolean(if_missing=False)
567 return _DefaultsForm
566 return _DefaultsForm
568
567
569
568
570 def AuthSettingsForm(localizer):
569 def AuthSettingsForm(localizer):
571 _ = localizer
570 _ = localizer
572
571
573 class _AuthSettingsForm(formencode.Schema):
572 class _AuthSettingsForm(formencode.Schema):
574 allow_extra_fields = True
573 allow_extra_fields = True
575 filter_extra_fields = True
574 filter_extra_fields = True
576 auth_plugins = All(v.ValidAuthPlugins(localizer),
575 auth_plugins = All(v.ValidAuthPlugins(localizer),
577 v.UniqueListFromString(localizer)(not_empty=True))
576 v.UniqueListFromString(localizer)(not_empty=True))
578 return _AuthSettingsForm
577 return _AuthSettingsForm
579
578
580
579
581 def UserExtraEmailForm(localizer):
580 def UserExtraEmailForm(localizer):
582 _ = localizer
581 _ = localizer
583
582
584 class _UserExtraEmailForm(formencode.Schema):
583 class _UserExtraEmailForm(formencode.Schema):
585 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
584 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
586 return _UserExtraEmailForm
585 return _UserExtraEmailForm
587
586
588
587
589 def UserExtraIpForm(localizer):
588 def UserExtraIpForm(localizer):
590 _ = localizer
589 _ = localizer
591
590
592 class _UserExtraIpForm(formencode.Schema):
591 class _UserExtraIpForm(formencode.Schema):
593 ip = v.ValidIp(localizer)(not_empty=True)
592 ip = v.ValidIp(localizer)(not_empty=True)
594 return _UserExtraIpForm
593 return _UserExtraIpForm
595
594
596
595
597 def PullRequestForm(localizer, repo_id):
596 def PullRequestForm(localizer, repo_id):
598 _ = localizer
597 _ = localizer
599
598
600 class ReviewerForm(formencode.Schema):
599 class ReviewerForm(formencode.Schema):
601 user_id = v.Int(not_empty=True)
600 user_id = v.Int(not_empty=True)
602 reasons = All()
601 reasons = All()
603 rules = All(v.UniqueList(localizer, convert=int)())
602 rules = All(v.UniqueList(localizer, convert=int)())
604 mandatory = v.StringBoolean()
603 mandatory = v.StringBoolean()
605 role = v.String(if_missing='reviewer')
604 role = v.String(if_missing='reviewer')
606
605
607 class ObserverForm(formencode.Schema):
606 class ObserverForm(formencode.Schema):
608 user_id = v.Int(not_empty=True)
607 user_id = v.Int(not_empty=True)
609 reasons = All()
608 reasons = All()
610 rules = All(v.UniqueList(localizer, convert=int)())
609 rules = All(v.UniqueList(localizer, convert=int)())
611 mandatory = v.StringBoolean()
610 mandatory = v.StringBoolean()
612 role = v.String(if_missing='observer')
611 role = v.String(if_missing='observer')
613
612
614 class _PullRequestForm(formencode.Schema):
613 class _PullRequestForm(formencode.Schema):
615 allow_extra_fields = True
614 allow_extra_fields = True
616 filter_extra_fields = True
615 filter_extra_fields = True
617
616
618 common_ancestor = v.UnicodeString(strip=True, required=True)
617 common_ancestor = v.UnicodeString(strip=True, required=True)
619 source_repo = v.UnicodeString(strip=True, required=True)
618 source_repo = v.UnicodeString(strip=True, required=True)
620 source_ref = v.UnicodeString(strip=True, required=True)
619 source_ref = v.UnicodeString(strip=True, required=True)
621 target_repo = v.UnicodeString(strip=True, required=True)
620 target_repo = v.UnicodeString(strip=True, required=True)
622 target_ref = v.UnicodeString(strip=True, required=True)
621 target_ref = v.UnicodeString(strip=True, required=True)
623 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
622 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
624 v.UniqueList(localizer)(not_empty=True))
623 v.UniqueList(localizer)(not_empty=True))
625 review_members = formencode.ForEach(ReviewerForm())
624 review_members = formencode.ForEach(ReviewerForm())
626 observer_members = formencode.ForEach(ObserverForm())
625 observer_members = formencode.ForEach(ObserverForm())
627 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
626 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
628 pullrequest_desc = v.UnicodeString(strip=True, required=False)
627 pullrequest_desc = v.UnicodeString(strip=True, required=False)
629 description_renderer = v.UnicodeString(strip=True, required=False)
628 description_renderer = v.UnicodeString(strip=True, required=False)
630
629
631 return _PullRequestForm
630 return _PullRequestForm
632
631
633
632
634 def IssueTrackerPatternsForm(localizer):
633 def IssueTrackerPatternsForm(localizer):
635 _ = localizer
634 _ = localizer
636
635
637 class _IssueTrackerPatternsForm(formencode.Schema):
636 class _IssueTrackerPatternsForm(formencode.Schema):
638 allow_extra_fields = True
637 allow_extra_fields = True
639 filter_extra_fields = False
638 filter_extra_fields = False
640 chained_validators = [v.ValidPattern(localizer)]
639 chained_validators = [v.ValidPattern(localizer)]
641 return _IssueTrackerPatternsForm
640 return _IssueTrackerPatternsForm
@@ -1,256 +1,256 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2013-2020 RhodeCode GmbH
3 # Copyright (C) 2013-2020 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 gist model for RhodeCode
22 gist model for RhodeCode
23 """
23 """
24
24
25 import os
25 import os
26 import time
26 import time
27 import logging
27 import logging
28 import traceback
28 import traceback
29 import shutil
29 import shutil
30
30
31 from pyramid.threadlocal import get_current_request
31 from pyramid.threadlocal import get_current_request
32
32
33 from rhodecode.lib.utils2 import (
33 from rhodecode.lib.utils2 import (
34 safe_unicode, unique_id, safe_int, time_to_datetime, AttributeDict)
34 safe_unicode, unique_id, safe_int, time_to_datetime, AttributeDict)
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.vcs import VCSError
36 from rhodecode.lib.vcs import VCSError
37 from rhodecode.model import BaseModel
37 from rhodecode.model import BaseModel
38 from rhodecode.model.db import Gist
38 from rhodecode.model.db import Gist
39 from rhodecode.model.repo import RepoModel
39 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.scm import ScmModel
40 from rhodecode.model.scm import ScmModel
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44 GIST_STORE_LOC = '.rc_gist_store'
44 GIST_STORE_LOC = '.rc_gist_store'
45 GIST_METADATA_FILE = '.rc_gist_metadata'
45 GIST_METADATA_FILE = '.rc_gist_metadata'
46
46
47
47
48 class GistModel(BaseModel):
48 class GistModel(BaseModel):
49 cls = Gist
49 cls = Gist
50 vcs_backend = 'hg'
50 vcs_backend = 'hg'
51
51
52 def _get_gist(self, gist):
52 def _get_gist(self, gist):
53 """
53 """
54 Helper method to get gist by ID, or gist_access_id as a fallback
54 Helper method to get gist by ID, or gist_access_id as a fallback
55
55
56 :param gist: GistID, gist_access_id, or Gist instance
56 :param gist: GistID, gist_access_id, or Gist instance
57 """
57 """
58 return self._get_instance(Gist, gist, callback=Gist.get_by_access_id)
58 return self._get_instance(Gist, gist, callback=Gist.get_by_access_id)
59
59
60 def __delete_gist(self, gist):
60 def __delete_gist(self, gist):
61 """
61 """
62 removes gist from filesystem
62 removes gist from filesystem
63
63
64 :param gist: gist object
64 :param gist: gist object
65 """
65 """
66 root_path = RepoModel().repos_path
66 root_path = RepoModel().repos_path
67 rm_path = os.path.join(root_path, GIST_STORE_LOC, gist.gist_access_id)
67 rm_path = os.path.join(root_path, GIST_STORE_LOC, gist.gist_access_id)
68 log.info("Removing %s", rm_path)
68 log.info("Removing %s", rm_path)
69 shutil.rmtree(rm_path)
69 shutil.rmtree(rm_path)
70
70
71 def _store_metadata(self, repo, gist_id, gist_access_id, user_id, username,
71 def _store_metadata(self, repo, gist_id, gist_access_id, user_id, username,
72 gist_type, gist_expires, gist_acl_level):
72 gist_type, gist_expires, gist_acl_level):
73 """
73 """
74 store metadata inside the gist repo, this can be later used for imports
74 store metadata inside the gist repo, this can be later used for imports
75 or gist identification. Currently we use this inside RhodeCode tools
75 or gist identification. Currently we use this inside RhodeCode tools
76 to do cleanup of gists that are in storage but not in database.
76 to do cleanup of gists that are in storage but not in database.
77 """
77 """
78 metadata = {
78 metadata = {
79 'metadata_version': '2',
79 'metadata_version': '2',
80 'gist_db_id': gist_id,
80 'gist_db_id': gist_id,
81 'gist_access_id': gist_access_id,
81 'gist_access_id': gist_access_id,
82 'gist_owner_id': user_id,
82 'gist_owner_id': user_id,
83 'gist_owner_username': username,
83 'gist_owner_username': username,
84 'gist_type': gist_type,
84 'gist_type': gist_type,
85 'gist_expires': gist_expires,
85 'gist_expires': gist_expires,
86 'gist_updated': time.time(),
86 'gist_updated': time.time(),
87 'gist_acl_level': gist_acl_level,
87 'gist_acl_level': gist_acl_level,
88 }
88 }
89 metadata_file = os.path.join(repo.path, '.hg', GIST_METADATA_FILE)
89 metadata_file = os.path.join(repo.path, '.hg', GIST_METADATA_FILE)
90 with open(metadata_file, 'wb') as f:
90 with open(metadata_file, 'wb') as f:
91 f.write(json.dumps(metadata))
91 f.write(json.dumps(metadata))
92
92
93 def get_gist(self, gist):
93 def get_gist(self, gist):
94 return self._get_gist(gist)
94 return self._get_gist(gist)
95
95
96 def get_gist_files(self, gist_access_id, revision=None):
96 def get_gist_files(self, gist_access_id, revision=None):
97 """
97 """
98 Get files for given gist
98 Get files for given gist
99
99
100 :param gist_access_id:
100 :param gist_access_id:
101 """
101 """
102 repo = Gist.get_by_access_id(gist_access_id)
102 repo = Gist.get_by_access_id(gist_access_id)
103 vcs_repo = repo.scm_instance()
103 vcs_repo = repo.scm_instance()
104 if not vcs_repo:
104 if not vcs_repo:
105 raise VCSError('Failed to load gist repository for {}'.format(repo))
105 raise VCSError('Failed to load gist repository for {}'.format(repo))
106
106
107 commit = vcs_repo.get_commit(commit_id=revision)
107 commit = vcs_repo.get_commit(commit_id=revision)
108 return commit, [n for n in commit.get_node('/')]
108 return commit, [n for n in commit.get_node('/')]
109
109
110 def create(self, description, owner, gist_mapping,
110 def create(self, description, owner, gist_mapping,
111 gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None,
111 gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None,
112 gist_acl_level=Gist.ACL_LEVEL_PRIVATE):
112 gist_acl_level=Gist.ACL_LEVEL_PRIVATE):
113 """
113 """
114 Create a gist
114 Create a gist
115
115
116 :param description: description of the gist
116 :param description: description of the gist
117 :param owner: user who created this gist
117 :param owner: user who created this gist
118 :param gist_mapping: mapping [{'filename': 'file1.txt', 'content': content}, ...}]
118 :param gist_mapping: mapping [{'filename': 'file1.txt', 'content': content}, ...}]
119 :param gist_type: type of gist private/public
119 :param gist_type: type of gist private/public
120 :param lifetime: in minutes, -1 == forever
120 :param lifetime: in minutes, -1 == forever
121 :param gist_acl_level: acl level for this gist
121 :param gist_acl_level: acl level for this gist
122 """
122 """
123 owner = self._get_user(owner)
123 owner = self._get_user(owner)
124 gist_id = safe_unicode(gist_id or unique_id(20))
124 gist_id = safe_unicode(gist_id or unique_id(20))
125 lifetime = safe_int(lifetime, -1)
125 lifetime = safe_int(lifetime, -1)
126 gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
126 gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
127 expiration = (time_to_datetime(gist_expires)
127 expiration = (time_to_datetime(gist_expires)
128 if gist_expires != -1 else 'forever')
128 if gist_expires != -1 else 'forever')
129 log.debug('set GIST expiration date to: %s', expiration)
129 log.debug('set GIST expiration date to: %s', expiration)
130 # create the Database version
130 # create the Database version
131 gist = Gist()
131 gist = Gist()
132 gist.gist_description = description
132 gist.gist_description = description
133 gist.gist_access_id = gist_id
133 gist.gist_access_id = gist_id
134 gist.gist_owner = owner.user_id
134 gist.gist_owner = owner.user_id
135 gist.gist_expires = gist_expires
135 gist.gist_expires = gist_expires
136 gist.gist_type = safe_unicode(gist_type)
136 gist.gist_type = safe_unicode(gist_type)
137 gist.acl_level = gist_acl_level
137 gist.acl_level = gist_acl_level
138 self.sa.add(gist)
138 self.sa.add(gist)
139 self.sa.flush()
139 self.sa.flush()
140 if gist_type == Gist.GIST_PUBLIC:
140 if gist_type == Gist.GIST_PUBLIC:
141 # use DB ID for easy to use GIST ID
141 # use DB ID for easy to use GIST ID
142 gist_id = safe_unicode(gist.gist_id)
142 gist_id = safe_unicode(gist.gist_id)
143 gist.gist_access_id = gist_id
143 gist.gist_access_id = gist_id
144 self.sa.add(gist)
144 self.sa.add(gist)
145
145
146 gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
146 gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
147 log.debug('Creating new %s GIST repo in %s', gist_type, gist_repo_path)
147 log.debug('Creating new %s GIST repo in %s', gist_type, gist_repo_path)
148 repo = RepoModel()._create_filesystem_repo(
148 repo = RepoModel()._create_filesystem_repo(
149 repo_name=gist_id, repo_type=self.vcs_backend, repo_group=GIST_STORE_LOC,
149 repo_name=gist_id, repo_type=self.vcs_backend, repo_group=GIST_STORE_LOC,
150 use_global_config=True)
150 use_global_config=True)
151
151
152 # now create single multifile commit
152 # now create single multifile commit
153 message = 'added file'
153 message = 'added file'
154 message += 's: ' if len(gist_mapping) > 1 else ': '
154 message += 's: ' if len(gist_mapping) > 1 else ': '
155 message += ', '.join([x for x in gist_mapping])
155 message += ', '.join([x for x in gist_mapping])
156
156
157 # fake RhodeCode Repository object
157 # fake RhodeCode Repository object
158 fake_repo = AttributeDict({
158 fake_repo = AttributeDict({
159 'repo_name': gist_repo_path,
159 'repo_name': gist_repo_path,
160 'scm_instance': lambda *args, **kwargs: repo,
160 'scm_instance': lambda *args, **kwargs: repo,
161 })
161 })
162
162
163 ScmModel().create_nodes(
163 ScmModel().create_nodes(
164 user=owner.user_id, repo=fake_repo,
164 user=owner.user_id, repo=fake_repo,
165 message=message,
165 message=message,
166 nodes=gist_mapping,
166 nodes=gist_mapping,
167 trigger_push_hook=False
167 trigger_push_hook=False
168 )
168 )
169
169
170 self._store_metadata(repo, gist.gist_id, gist.gist_access_id,
170 self._store_metadata(repo, gist.gist_id, gist.gist_access_id,
171 owner.user_id, owner.username, gist.gist_type,
171 owner.user_id, owner.username, gist.gist_type,
172 gist.gist_expires, gist_acl_level)
172 gist.gist_expires, gist_acl_level)
173 return gist
173 return gist
174
174
175 def delete(self, gist, fs_remove=True):
175 def delete(self, gist, fs_remove=True):
176 gist = self._get_gist(gist)
176 gist = self._get_gist(gist)
177 try:
177 try:
178 self.sa.delete(gist)
178 self.sa.delete(gist)
179 if fs_remove:
179 if fs_remove:
180 self.__delete_gist(gist)
180 self.__delete_gist(gist)
181 else:
181 else:
182 log.debug('skipping removal from filesystem')
182 log.debug('skipping removal from filesystem')
183 except Exception:
183 except Exception:
184 log.error(traceback.format_exc())
184 log.error(traceback.format_exc())
185 raise
185 raise
186
186
187 def update(self, gist, description, owner, gist_mapping, lifetime,
187 def update(self, gist, description, owner, gist_mapping, lifetime,
188 gist_acl_level):
188 gist_acl_level):
189 gist = self._get_gist(gist)
189 gist = self._get_gist(gist)
190 gist_repo = gist.scm_instance()
190 gist_repo = gist.scm_instance()
191
191
192 if lifetime == 0: # preserve old value
192 if lifetime == 0: # preserve old value
193 gist_expires = gist.gist_expires
193 gist_expires = gist.gist_expires
194 else:
194 else:
195 gist_expires = (
195 gist_expires = (
196 time.time() + (lifetime * 60) if lifetime != -1 else -1)
196 time.time() + (lifetime * 60) if lifetime != -1 else -1)
197
197
198 # calculate operation type based on given data
198 # calculate operation type based on given data
199 gist_mapping_op = {}
199 gist_mapping_op = {}
200 for k, v in gist_mapping.items():
200 for k, v in gist_mapping.items():
201 # add, mod, del
201 # add, mod, del
202 if not v['filename_org'] and v['filename']:
202 if not v['filename_org'] and v['filename']:
203 op = 'add'
203 op = 'add'
204 elif v['filename_org'] and not v['filename']:
204 elif v['filename_org'] and not v['filename']:
205 op = 'del'
205 op = 'del'
206 else:
206 else:
207 op = 'mod'
207 op = 'mod'
208
208
209 v['op'] = op
209 v['op'] = op
210 gist_mapping_op[k] = v
210 gist_mapping_op[k] = v
211
211
212 gist.gist_description = description
212 gist.gist_description = description
213 gist.gist_expires = gist_expires
213 gist.gist_expires = gist_expires
214 gist.owner = owner
214 gist.owner = owner
215 gist.acl_level = gist_acl_level
215 gist.acl_level = gist_acl_level
216 self.sa.add(gist)
216 self.sa.add(gist)
217 self.sa.flush()
217 self.sa.flush()
218
218
219 message = 'updated file'
219 message = 'updated file'
220 message += 's: ' if len(gist_mapping) > 1 else ': '
220 message += 's: ' if len(gist_mapping) > 1 else ': '
221 message += ', '.join([x for x in gist_mapping])
221 message += ', '.join([x for x in gist_mapping])
222
222
223 # fake RhodeCode Repository object
223 # fake RhodeCode Repository object
224 fake_repo = AttributeDict({
224 fake_repo = AttributeDict({
225 'repo_name': gist_repo.path,
225 'repo_name': gist_repo.path,
226 'scm_instance': lambda *args, **kwargs: gist_repo,
226 'scm_instance': lambda *args, **kwargs: gist_repo,
227 })
227 })
228
228
229 self._store_metadata(gist_repo, gist.gist_id, gist.gist_access_id,
229 self._store_metadata(gist_repo, gist.gist_id, gist.gist_access_id,
230 owner.user_id, owner.username, gist.gist_type,
230 owner.user_id, owner.username, gist.gist_type,
231 gist.gist_expires, gist_acl_level)
231 gist.gist_expires, gist_acl_level)
232
232
233 # this can throw NodeNotChangedError, if changes we're trying to commit
233 # this can throw NodeNotChangedError, if changes we're trying to commit
234 # are not actually changes...
234 # are not actually changes...
235 ScmModel().update_nodes(
235 ScmModel().update_nodes(
236 user=owner.user_id,
236 user=owner.user_id,
237 repo=fake_repo,
237 repo=fake_repo,
238 message=message,
238 message=message,
239 nodes=gist_mapping_op,
239 nodes=gist_mapping_op,
240 trigger_push_hook=False
240 trigger_push_hook=False
241 )
241 )
242
242
243 return gist
243 return gist
244
244
245 def get_url(self, gist, request=None):
245 def get_url(self, gist, request=None):
246 import rhodecode
246 import rhodecode
247
247
248 if not request:
248 if not request:
249 request = get_current_request()
249 request = get_current_request()
250
250
251 alias_url = rhodecode.CONFIG.get('gist_alias_url')
251 alias_url = rhodecode.CONFIG.get('gist_alias_url')
252 if alias_url:
252 if alias_url:
253 return alias_url.replace('{gistid}', gist.gist_access_id)
253 return alias_url.replace('{gistid}', gist.gist_access_id)
254
254
255 return request.route_url('gist_show', gist_id=gist.gist_access_id)
255 return request.route_url('gist_show', gist_id=gist.gist_access_id)
256
256
@@ -1,241 +1,241 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 Model for integrations
23 Model for integrations
24 """
24 """
25
25
26
26
27 import logging
27 import logging
28
28
29 from sqlalchemy import or_, and_
29 from sqlalchemy import or_, and_
30
30
31 import rhodecode
31 import rhodecode
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.integrations.types.base import EEIntegration
33 from rhodecode.integrations.types.base import EEIntegration
34 from rhodecode.lib.caching_query import FromCache
34 from rhodecode.lib.caching_query import FromCache
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import Integration, Repository, RepoGroup, true, false, case
36 from rhodecode.model.db import Integration, Repository, RepoGroup, true, false, case
37 from rhodecode.integrations import integration_type_registry
37 from rhodecode.integrations import integration_type_registry
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class IntegrationModel(BaseModel):
42 class IntegrationModel(BaseModel):
43
43
44 cls = Integration
44 cls = Integration
45
45
46 def __get_integration(self, integration):
46 def __get_integration(self, integration):
47 if isinstance(integration, Integration):
47 if isinstance(integration, Integration):
48 return integration
48 return integration
49 elif isinstance(integration, int):
49 elif isinstance(integration, int):
50 return self.sa.query(Integration).get(integration)
50 return self.sa.query(Integration).get(integration)
51 else:
51 else:
52 if integration:
52 if integration:
53 raise Exception('integration must be int or Instance'
53 raise Exception('integration must be int or Instance'
54 ' of Integration got %s' % type(integration))
54 ' of Integration got %s' % type(integration))
55
55
56 def create(self, IntegrationType, name, enabled, repo, repo_group,
56 def create(self, IntegrationType, name, enabled, repo, repo_group,
57 child_repos_only, settings):
57 child_repos_only, settings):
58 """ Create an IntegrationType integration """
58 """ Create an IntegrationType integration """
59 integration = Integration()
59 integration = Integration()
60 integration.integration_type = IntegrationType.key
60 integration.integration_type = IntegrationType.key
61 self.sa.add(integration)
61 self.sa.add(integration)
62 self.update_integration(integration, name, enabled, repo, repo_group,
62 self.update_integration(integration, name, enabled, repo, repo_group,
63 child_repos_only, settings)
63 child_repos_only, settings)
64 self.sa.commit()
64 self.sa.commit()
65 return integration
65 return integration
66
66
67 def update_integration(self, integration, name, enabled, repo, repo_group,
67 def update_integration(self, integration, name, enabled, repo, repo_group,
68 child_repos_only, settings):
68 child_repos_only, settings):
69 integration = self.__get_integration(integration)
69 integration = self.__get_integration(integration)
70
70
71 integration.repo = repo
71 integration.repo = repo
72 integration.repo_group = repo_group
72 integration.repo_group = repo_group
73 integration.child_repos_only = child_repos_only
73 integration.child_repos_only = child_repos_only
74 integration.name = name
74 integration.name = name
75 integration.enabled = enabled
75 integration.enabled = enabled
76 integration.settings = settings
76 integration.settings = settings
77
77
78 return integration
78 return integration
79
79
80 def delete(self, integration):
80 def delete(self, integration):
81 integration = self.__get_integration(integration)
81 integration = self.__get_integration(integration)
82 if integration:
82 if integration:
83 self.sa.delete(integration)
83 self.sa.delete(integration)
84 return True
84 return True
85 return False
85 return False
86
86
87 def get_integration_handler(self, integration):
87 def get_integration_handler(self, integration):
88 TypeClass = integration_type_registry.get(integration.integration_type)
88 TypeClass = integration_type_registry.get(integration.integration_type)
89 if not TypeClass:
89 if not TypeClass:
90 log.error('No class could be found for integration type: {}'.format(
90 log.error('No class could be found for integration type: {}'.format(
91 integration.integration_type))
91 integration.integration_type))
92 return None
92 return None
93 elif isinstance(TypeClass, EEIntegration) or issubclass(TypeClass, EEIntegration):
93 elif isinstance(TypeClass, EEIntegration) or issubclass(TypeClass, EEIntegration):
94 log.error('EE integration cannot be '
94 log.error('EE integration cannot be '
95 'executed for integration type: {}'.format(
95 'executed for integration type: {}'.format(
96 integration.integration_type))
96 integration.integration_type))
97 return None
97 return None
98
98
99 return TypeClass(integration.settings)
99 return TypeClass(integration.settings)
100
100
101 def send_event(self, integration, event):
101 def send_event(self, integration, event):
102 """ Send an event to an integration """
102 """ Send an event to an integration """
103 handler = self.get_integration_handler(integration)
103 handler = self.get_integration_handler(integration)
104 if handler:
104 if handler:
105 log.debug(
105 log.debug(
106 'events: sending event %s on integration %s using handler %s',
106 'events: sending event %s on integration %s using handler %s',
107 event, integration, handler)
107 event, integration, handler)
108 handler.send_event(event)
108 handler.send_event(event)
109
109
110 def get_integrations(self, scope, IntegrationType=None):
110 def get_integrations(self, scope, IntegrationType=None):
111 """
111 """
112 Return integrations for a scope, which must be one of:
112 Return integrations for a scope, which must be one of:
113
113
114 'all' - every integration, global/repogroup/repo
114 'all' - every integration, global/repogroup/repo
115 'global' - global integrations only
115 'global' - global integrations only
116 <Repository> instance - integrations for this repo only
116 <Repository> instance - integrations for this repo only
117 <RepoGroup> instance - integrations for this repogroup only
117 <RepoGroup> instance - integrations for this repogroup only
118 """
118 """
119
119
120 if isinstance(scope, Repository):
120 if isinstance(scope, Repository):
121 query = self.sa.query(Integration).filter(
121 query = self.sa.query(Integration).filter(
122 Integration.repo == scope)
122 Integration.repo == scope)
123 elif isinstance(scope, RepoGroup):
123 elif isinstance(scope, RepoGroup):
124 query = self.sa.query(Integration).filter(
124 query = self.sa.query(Integration).filter(
125 Integration.repo_group == scope)
125 Integration.repo_group == scope)
126 elif scope == 'global':
126 elif scope == 'global':
127 # global integrations
127 # global integrations
128 query = self.sa.query(Integration).filter(
128 query = self.sa.query(Integration).filter(
129 and_(Integration.repo_id == None, Integration.repo_group_id == None)
129 and_(Integration.repo_id == None, Integration.repo_group_id == None)
130 )
130 )
131 elif scope == 'root-repos':
131 elif scope == 'root-repos':
132 query = self.sa.query(Integration).filter(
132 query = self.sa.query(Integration).filter(
133 and_(Integration.repo_id == None,
133 and_(Integration.repo_id == None,
134 Integration.repo_group_id == None,
134 Integration.repo_group_id == None,
135 Integration.child_repos_only == true())
135 Integration.child_repos_only == true())
136 )
136 )
137 elif scope == 'all':
137 elif scope == 'all':
138 query = self.sa.query(Integration)
138 query = self.sa.query(Integration)
139 else:
139 else:
140 raise Exception(
140 raise Exception(
141 "invalid `scope`, must be one of: "
141 "invalid `scope`, must be one of: "
142 "['global', 'all', <Repository>, <RepoGroup>]")
142 "['global', 'all', <Repository>, <RepoGroup>]")
143
143
144 if IntegrationType is not None:
144 if IntegrationType is not None:
145 query = query.filter(
145 query = query.filter(
146 Integration.integration_type==IntegrationType.key)
146 Integration.integration_type==IntegrationType.key)
147
147
148 result = []
148 result = []
149 for integration in query.all():
149 for integration in query.all():
150 IntType = integration_type_registry.get(integration.integration_type)
150 IntType = integration_type_registry.get(integration.integration_type)
151 result.append((IntType, integration))
151 result.append((IntType, integration))
152 return result
152 return result
153
153
154 def get_for_event(self, event, cache=False):
154 def get_for_event(self, event, cache=False):
155 """
155 """
156 Get integrations that match an event
156 Get integrations that match an event
157 """
157 """
158 # base query
158 # base query
159 query = self.sa.query(
159 query = self.sa.query(
160 Integration
160 Integration
161 ).filter(
161 ).filter(
162 Integration.enabled == true()
162 Integration.enabled == true()
163 )
163 )
164
164
165 global_integrations_filter = and_(
165 global_integrations_filter = and_(
166 Integration.repo_id == None,
166 Integration.repo_id == None,
167 Integration.repo_group_id == None,
167 Integration.repo_group_id == None,
168 Integration.child_repos_only == false(),
168 Integration.child_repos_only == false(),
169 )
169 )
170
170
171 if isinstance(event, events.RepoEvent):
171 if isinstance(event, events.RepoEvent):
172 root_repos_integrations_filter = and_(
172 root_repos_integrations_filter = and_(
173 Integration.repo_id == None,
173 Integration.repo_id == None,
174 Integration.repo_group_id == None,
174 Integration.repo_group_id == None,
175 Integration.child_repos_only == true(),
175 Integration.child_repos_only == true(),
176 )
176 )
177
177
178 clauses = [
178 clauses = [
179 global_integrations_filter,
179 global_integrations_filter,
180 ]
180 ]
181 cases = [
181 cases = [
182 (global_integrations_filter, 1),
182 (global_integrations_filter, 1),
183 (root_repos_integrations_filter, 2),
183 (root_repos_integrations_filter, 2),
184 ]
184 ]
185
185
186 # repo group integrations
186 # repo group integrations
187 if event.repo.group:
187 if event.repo.group:
188 # repo group with only root level repos
188 # repo group with only root level repos
189 group_child_repos_filter = and_(
189 group_child_repos_filter = and_(
190 Integration.repo_group_id == event.repo.group.group_id,
190 Integration.repo_group_id == event.repo.group.group_id,
191 Integration.child_repos_only == true()
191 Integration.child_repos_only == true()
192 )
192 )
193
193
194 clauses.append(group_child_repos_filter)
194 clauses.append(group_child_repos_filter)
195 cases.append(
195 cases.append(
196 (group_child_repos_filter, 3),
196 (group_child_repos_filter, 3),
197 )
197 )
198
198
199 # repo group cascade to kids
199 # repo group cascade to kids
200 group_recursive_repos_filter = and_(
200 group_recursive_repos_filter = and_(
201 Integration.repo_group_id.in_(
201 Integration.repo_group_id.in_(
202 [group.group_id for group in event.repo.groups_with_parents]
202 [group.group_id for group in event.repo.groups_with_parents]
203 ),
203 ),
204 Integration.child_repos_only == false()
204 Integration.child_repos_only == false()
205 )
205 )
206 clauses.append(group_recursive_repos_filter)
206 clauses.append(group_recursive_repos_filter)
207 cases.append(
207 cases.append(
208 (group_recursive_repos_filter, 4),
208 (group_recursive_repos_filter, 4),
209 )
209 )
210
210
211 if not event.repo.group: # root repo
211 if not event.repo.group: # root repo
212 clauses.append(root_repos_integrations_filter)
212 clauses.append(root_repos_integrations_filter)
213
213
214 # repo integrations
214 # repo integrations
215 if event.repo.repo_id: # pre create events dont have a repo_id yet
215 if event.repo.repo_id: # pre create events dont have a repo_id yet
216 specific_repo_filter = Integration.repo_id == event.repo.repo_id
216 specific_repo_filter = Integration.repo_id == event.repo.repo_id
217 clauses.append(specific_repo_filter)
217 clauses.append(specific_repo_filter)
218 cases.append(
218 cases.append(
219 (specific_repo_filter, 5),
219 (specific_repo_filter, 5),
220 )
220 )
221
221
222 order_by_criterion = case(cases)
222 order_by_criterion = case(cases)
223
223
224 query = query.filter(or_(*clauses))
224 query = query.filter(or_(*clauses))
225 query = query.order_by(order_by_criterion)
225 query = query.order_by(order_by_criterion)
226
226
227 if cache:
227 if cache:
228 cache_key = "get_enabled_repo_integrations_%i" % event.repo.repo_id
228 cache_key = "get_enabled_repo_integrations_%i" % event.repo.repo_id
229 query = query.options(
229 query = query.options(
230 FromCache("sql_cache_short", cache_key))
230 FromCache("sql_cache_short", cache_key))
231 else: # only global integrations
231 else: # only global integrations
232 order_by_criterion = Integration.integration_id
232 order_by_criterion = Integration.integration_id
233
233
234 query = query.filter(global_integrations_filter)
234 query = query.filter(global_integrations_filter)
235 query = query.order_by(order_by_criterion)
235 query = query.order_by(order_by_criterion)
236 if cache:
236 if cache:
237 query = query.options(
237 query = query.options(
238 FromCache("sql_cache_short", "get_enabled_global_integrations"))
238 FromCache("sql_cache_short", "get_enabled_global_integrations"))
239
239
240 result = query.all()
240 result = query.all()
241 return result
241 return result
@@ -1,56 +1,55 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 """
20 """
22 SQLAlchemy Metadata and Session object
21 SQLAlchemy Metadata and Session object
23 """
22 """
24
23
25 from sqlalchemy.orm import declarative_base
24 from sqlalchemy.orm import declarative_base
26 from sqlalchemy.orm import scoped_session, sessionmaker
25 from sqlalchemy.orm import scoped_session, sessionmaker
27 from sqlalchemy.orm import Session as SASession
26 from sqlalchemy.orm import Session as SASession
28 from rhodecode.lib.caching_query import ORMCache
27 from rhodecode.lib.caching_query import ORMCache
29
28
30
29
31 __all__ = ['Base', 'Session', 'raw_query_executor']
30 __all__ = ['Base', 'Session', 'raw_query_executor']
32
31
33 # scoped_session. Apply our custom CachingQuery class to it,
32 # scoped_session. Apply our custom CachingQuery class to it,
34 # using a callable that will associate the dictionary
33 # using a callable that will associate the dictionary
35 # of regions with the Query.
34 # of regions with the Query.
36 # to use cache use this in query
35 # to use cache use this in query
37 # .options(FromCache("sqlalchemy_cache_type", "cachekey"))
36 # .options(FromCache("sqlalchemy_cache_type", "cachekey"))
38 Session = scoped_session(
37 Session = scoped_session(
39 sessionmaker(
38 sessionmaker(
40 expire_on_commit=True,
39 expire_on_commit=True,
41 )
40 )
42 )
41 )
43
42
44 # pass empty regions so we can fetch it on-demand inside ORMCache
43 # pass empty regions so we can fetch it on-demand inside ORMCache
45 cache = ORMCache(regions={})
44 cache = ORMCache(regions={})
46 cache.listen_on_session(Session)
45 cache.listen_on_session(Session)
47
46
48
47
49 # The declarative Base
48 # The declarative Base
50 Base = declarative_base()
49 Base = declarative_base()
51
50
52
51
53 def raw_query_executor():
52 def raw_query_executor():
54 engine = Base.metadata.bind
53 engine = Base.metadata.bind
55 session = SASession(engine)
54 session = SASession(engine)
56 return session
55 return session
@@ -1,453 +1,453 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 Model for notifications
23 Model for notifications
24 """
24 """
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 import premailer
29 import premailer
30 from pyramid.threadlocal import get_current_request
30 from pyramid.threadlocal import get_current_request
31 from sqlalchemy.sql.expression import false, true
31 from sqlalchemy.sql.expression import false, true
32
32
33 import rhodecode
33 import rhodecode
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import Notification, User, UserNotification
36 from rhodecode.model.db import Notification, User, UserNotification
37 from rhodecode.model.meta import Session
37 from rhodecode.model.meta import Session
38 from rhodecode.translation import TranslationString
38 from rhodecode.translation import TranslationString
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class NotificationModel(BaseModel):
43 class NotificationModel(BaseModel):
44
44
45 cls = Notification
45 cls = Notification
46
46
47 def __get_notification(self, notification):
47 def __get_notification(self, notification):
48 if isinstance(notification, Notification):
48 if isinstance(notification, Notification):
49 return notification
49 return notification
50 elif isinstance(notification, int):
50 elif isinstance(notification, int):
51 return Notification.get(notification)
51 return Notification.get(notification)
52 else:
52 else:
53 if notification:
53 if notification:
54 raise Exception('notification must be int or Instance'
54 raise Exception('notification must be int or Instance'
55 ' of Notification got %s' % type(notification))
55 ' of Notification got %s' % type(notification))
56
56
57 def create(
57 def create(
58 self, created_by, notification_subject='', notification_body='',
58 self, created_by, notification_subject='', notification_body='',
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
60 mention_recipients=None, with_email=True, email_kwargs=None):
60 mention_recipients=None, with_email=True, email_kwargs=None):
61 """
61 """
62
62
63 Creates notification of given type
63 Creates notification of given type
64
64
65 :param created_by: int, str or User instance. User who created this
65 :param created_by: int, str or User instance. User who created this
66 notification
66 notification
67 :param notification_subject: subject of notification itself,
67 :param notification_subject: subject of notification itself,
68 it will be generated automatically from notification_type if not specified
68 it will be generated automatically from notification_type if not specified
69 :param notification_body: body of notification text
69 :param notification_body: body of notification text
70 it will be generated automatically from notification_type if not specified
70 it will be generated automatically from notification_type if not specified
71 :param notification_type: type of notification, based on that we
71 :param notification_type: type of notification, based on that we
72 pick templates
72 pick templates
73 :param recipients: list of int, str or User objects, when None
73 :param recipients: list of int, str or User objects, when None
74 is given send to all admins
74 is given send to all admins
75 :param mention_recipients: list of int, str or User objects,
75 :param mention_recipients: list of int, str or User objects,
76 that were mentioned
76 that were mentioned
77 :param with_email: send email with this notification
77 :param with_email: send email with this notification
78 :param email_kwargs: dict with arguments to generate email
78 :param email_kwargs: dict with arguments to generate email
79 """
79 """
80
80
81 from rhodecode.lib.celerylib import tasks, run_task
81 from rhodecode.lib.celerylib import tasks, run_task
82
82
83 if recipients and not getattr(recipients, '__iter__', False):
83 if recipients and not getattr(recipients, '__iter__', False):
84 raise Exception('recipients must be an iterable object')
84 raise Exception('recipients must be an iterable object')
85
85
86 if not (notification_subject and notification_body) and not notification_type:
86 if not (notification_subject and notification_body) and not notification_type:
87 raise ValueError('notification_subject, and notification_body '
87 raise ValueError('notification_subject, and notification_body '
88 'cannot be empty when notification_type is not specified')
88 'cannot be empty when notification_type is not specified')
89
89
90 created_by_obj = self._get_user(created_by)
90 created_by_obj = self._get_user(created_by)
91
91
92 if not created_by_obj:
92 if not created_by_obj:
93 raise Exception('unknown user %s' % created_by)
93 raise Exception('unknown user %s' % created_by)
94
94
95 # default MAIN body if not given
95 # default MAIN body if not given
96 email_kwargs = email_kwargs or {'body': notification_body}
96 email_kwargs = email_kwargs or {'body': notification_body}
97 mention_recipients = mention_recipients or set()
97 mention_recipients = mention_recipients or set()
98
98
99 if recipients is None:
99 if recipients is None:
100 # recipients is None means to all admins
100 # recipients is None means to all admins
101 recipients_objs = User.query().filter(User.admin == true()).all()
101 recipients_objs = User.query().filter(User.admin == true()).all()
102 log.debug('sending notifications %s to admins: %s',
102 log.debug('sending notifications %s to admins: %s',
103 notification_type, recipients_objs)
103 notification_type, recipients_objs)
104 else:
104 else:
105 recipients_objs = set()
105 recipients_objs = set()
106 for u in recipients:
106 for u in recipients:
107 obj = self._get_user(u)
107 obj = self._get_user(u)
108 if obj:
108 if obj:
109 recipients_objs.add(obj)
109 recipients_objs.add(obj)
110 else: # we didn't find this user, log the error and carry on
110 else: # we didn't find this user, log the error and carry on
111 log.error('cannot notify unknown user %r', u)
111 log.error('cannot notify unknown user %r', u)
112
112
113 if not recipients_objs:
113 if not recipients_objs:
114 raise Exception('no valid recipients specified')
114 raise Exception('no valid recipients specified')
115
115
116 log.debug('sending notifications %s to %s',
116 log.debug('sending notifications %s to %s',
117 notification_type, recipients_objs)
117 notification_type, recipients_objs)
118
118
119 # add mentioned users into recipients
119 # add mentioned users into recipients
120 final_recipients = set(recipients_objs).union(mention_recipients)
120 final_recipients = set(recipients_objs).union(mention_recipients)
121
121
122 (subject, email_body, email_body_plaintext) = \
122 (subject, email_body, email_body_plaintext) = \
123 EmailNotificationModel().render_email(notification_type, **email_kwargs)
123 EmailNotificationModel().render_email(notification_type, **email_kwargs)
124
124
125 if not notification_subject:
125 if not notification_subject:
126 notification_subject = subject
126 notification_subject = subject
127
127
128 if not notification_body:
128 if not notification_body:
129 notification_body = email_body_plaintext
129 notification_body = email_body_plaintext
130
130
131 notification = Notification.create(
131 notification = Notification.create(
132 created_by=created_by_obj, subject=notification_subject,
132 created_by=created_by_obj, subject=notification_subject,
133 body=notification_body, recipients=final_recipients,
133 body=notification_body, recipients=final_recipients,
134 type_=notification_type
134 type_=notification_type
135 )
135 )
136
136
137 if not with_email: # skip sending email, and just create notification
137 if not with_email: # skip sending email, and just create notification
138 return notification
138 return notification
139
139
140 # don't send email to person who created this comment
140 # don't send email to person who created this comment
141 rec_objs = set(recipients_objs).difference({created_by_obj})
141 rec_objs = set(recipients_objs).difference({created_by_obj})
142
142
143 # now notify all recipients in question
143 # now notify all recipients in question
144
144
145 for recipient in rec_objs.union(mention_recipients):
145 for recipient in rec_objs.union(mention_recipients):
146 # inject current recipient
146 # inject current recipient
147 email_kwargs['recipient'] = recipient
147 email_kwargs['recipient'] = recipient
148 email_kwargs['mention'] = recipient in mention_recipients
148 email_kwargs['mention'] = recipient in mention_recipients
149 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
149 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
150 notification_type, **email_kwargs)
150 notification_type, **email_kwargs)
151
151
152 extra_headers = None
152 extra_headers = None
153 if 'thread_ids' in email_kwargs:
153 if 'thread_ids' in email_kwargs:
154 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
154 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
155
155
156 log.debug('Creating notification email task for user:`%s`', recipient)
156 log.debug('Creating notification email task for user:`%s`', recipient)
157 task = run_task(tasks.send_email, recipient.email, subject,
157 task = run_task(tasks.send_email, recipient.email, subject,
158 email_body_plaintext, email_body, extra_headers=extra_headers)
158 email_body_plaintext, email_body, extra_headers=extra_headers)
159 log.debug('Created email task: %s', task)
159 log.debug('Created email task: %s', task)
160
160
161 return notification
161 return notification
162
162
163 def delete(self, user, notification):
163 def delete(self, user, notification):
164 # we don't want to remove actual notification just the assignment
164 # we don't want to remove actual notification just the assignment
165 try:
165 try:
166 notification = self.__get_notification(notification)
166 notification = self.__get_notification(notification)
167 user = self._get_user(user)
167 user = self._get_user(user)
168 if notification and user:
168 if notification and user:
169 obj = UserNotification.query()\
169 obj = UserNotification.query()\
170 .filter(UserNotification.user == user)\
170 .filter(UserNotification.user == user)\
171 .filter(UserNotification.notification == notification)\
171 .filter(UserNotification.notification == notification)\
172 .one()
172 .one()
173 Session().delete(obj)
173 Session().delete(obj)
174 return True
174 return True
175 except Exception:
175 except Exception:
176 log.error(traceback.format_exc())
176 log.error(traceback.format_exc())
177 raise
177 raise
178
178
179 def get_for_user(self, user, filter_=None):
179 def get_for_user(self, user, filter_=None):
180 """
180 """
181 Get mentions for given user, filter them if filter dict is given
181 Get mentions for given user, filter them if filter dict is given
182 """
182 """
183 user = self._get_user(user)
183 user = self._get_user(user)
184
184
185 q = UserNotification.query()\
185 q = UserNotification.query()\
186 .filter(UserNotification.user == user)\
186 .filter(UserNotification.user == user)\
187 .join((
187 .join((
188 Notification, UserNotification.notification_id ==
188 Notification, UserNotification.notification_id ==
189 Notification.notification_id))
189 Notification.notification_id))
190 if filter_ == ['all']:
190 if filter_ == ['all']:
191 q = q # no filter
191 q = q # no filter
192 elif filter_ == ['unread']:
192 elif filter_ == ['unread']:
193 q = q.filter(UserNotification.read == false())
193 q = q.filter(UserNotification.read == false())
194 elif filter_:
194 elif filter_:
195 q = q.filter(Notification.type_.in_(filter_))
195 q = q.filter(Notification.type_.in_(filter_))
196
196
197 return q
197 return q
198
198
199 def mark_read(self, user, notification):
199 def mark_read(self, user, notification):
200 try:
200 try:
201 notification = self.__get_notification(notification)
201 notification = self.__get_notification(notification)
202 user = self._get_user(user)
202 user = self._get_user(user)
203 if notification and user:
203 if notification and user:
204 obj = UserNotification.query()\
204 obj = UserNotification.query()\
205 .filter(UserNotification.user == user)\
205 .filter(UserNotification.user == user)\
206 .filter(UserNotification.notification == notification)\
206 .filter(UserNotification.notification == notification)\
207 .one()
207 .one()
208 obj.read = True
208 obj.read = True
209 Session().add(obj)
209 Session().add(obj)
210 return True
210 return True
211 except Exception:
211 except Exception:
212 log.error(traceback.format_exc())
212 log.error(traceback.format_exc())
213 raise
213 raise
214
214
215 def mark_all_read_for_user(self, user, filter_=None):
215 def mark_all_read_for_user(self, user, filter_=None):
216 user = self._get_user(user)
216 user = self._get_user(user)
217 q = UserNotification.query()\
217 q = UserNotification.query()\
218 .filter(UserNotification.user == user)\
218 .filter(UserNotification.user == user)\
219 .filter(UserNotification.read == false())\
219 .filter(UserNotification.read == false())\
220 .join((
220 .join((
221 Notification, UserNotification.notification_id ==
221 Notification, UserNotification.notification_id ==
222 Notification.notification_id))
222 Notification.notification_id))
223 if filter_ == ['unread']:
223 if filter_ == ['unread']:
224 q = q.filter(UserNotification.read == false())
224 q = q.filter(UserNotification.read == false())
225 elif filter_:
225 elif filter_:
226 q = q.filter(Notification.type_.in_(filter_))
226 q = q.filter(Notification.type_.in_(filter_))
227
227
228 # this is a little inefficient but sqlalchemy doesn't support
228 # this is a little inefficient but sqlalchemy doesn't support
229 # update on joined tables :(
229 # update on joined tables :(
230 for obj in q.all():
230 for obj in q.all():
231 obj.read = True
231 obj.read = True
232 Session().add(obj)
232 Session().add(obj)
233
233
234 def get_unread_cnt_for_user(self, user):
234 def get_unread_cnt_for_user(self, user):
235 user = self._get_user(user)
235 user = self._get_user(user)
236 return UserNotification.query()\
236 return UserNotification.query()\
237 .filter(UserNotification.read == false())\
237 .filter(UserNotification.read == false())\
238 .filter(UserNotification.user == user).count()
238 .filter(UserNotification.user == user).count()
239
239
240 def get_unread_for_user(self, user):
240 def get_unread_for_user(self, user):
241 user = self._get_user(user)
241 user = self._get_user(user)
242 return [x.notification for x in UserNotification.query()
242 return [x.notification for x in UserNotification.query()
243 .filter(UserNotification.read == false())
243 .filter(UserNotification.read == false())
244 .filter(UserNotification.user == user).all()]
244 .filter(UserNotification.user == user).all()]
245
245
246 def get_user_notification(self, user, notification):
246 def get_user_notification(self, user, notification):
247 user = self._get_user(user)
247 user = self._get_user(user)
248 notification = self.__get_notification(notification)
248 notification = self.__get_notification(notification)
249
249
250 return UserNotification.query()\
250 return UserNotification.query()\
251 .filter(UserNotification.notification == notification)\
251 .filter(UserNotification.notification == notification)\
252 .filter(UserNotification.user == user).scalar()
252 .filter(UserNotification.user == user).scalar()
253
253
254 def make_description(self, notification, translate, show_age=True):
254 def make_description(self, notification, translate, show_age=True):
255 """
255 """
256 Creates a human readable description based on properties
256 Creates a human readable description based on properties
257 of notification object
257 of notification object
258 """
258 """
259 _ = translate
259 _ = translate
260 _map = {
260 _map = {
261 notification.TYPE_CHANGESET_COMMENT: [
261 notification.TYPE_CHANGESET_COMMENT: [
262 _('%(user)s commented on commit %(date_or_age)s'),
262 _('%(user)s commented on commit %(date_or_age)s'),
263 _('%(user)s commented on commit at %(date_or_age)s'),
263 _('%(user)s commented on commit at %(date_or_age)s'),
264 ],
264 ],
265 notification.TYPE_MESSAGE: [
265 notification.TYPE_MESSAGE: [
266 _('%(user)s sent message %(date_or_age)s'),
266 _('%(user)s sent message %(date_or_age)s'),
267 _('%(user)s sent message at %(date_or_age)s'),
267 _('%(user)s sent message at %(date_or_age)s'),
268 ],
268 ],
269 notification.TYPE_MENTION: [
269 notification.TYPE_MENTION: [
270 _('%(user)s mentioned you %(date_or_age)s'),
270 _('%(user)s mentioned you %(date_or_age)s'),
271 _('%(user)s mentioned you at %(date_or_age)s'),
271 _('%(user)s mentioned you at %(date_or_age)s'),
272 ],
272 ],
273 notification.TYPE_REGISTRATION: [
273 notification.TYPE_REGISTRATION: [
274 _('%(user)s registered in RhodeCode %(date_or_age)s'),
274 _('%(user)s registered in RhodeCode %(date_or_age)s'),
275 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
275 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
276 ],
276 ],
277 notification.TYPE_PULL_REQUEST: [
277 notification.TYPE_PULL_REQUEST: [
278 _('%(user)s opened new pull request %(date_or_age)s'),
278 _('%(user)s opened new pull request %(date_or_age)s'),
279 _('%(user)s opened new pull request at %(date_or_age)s'),
279 _('%(user)s opened new pull request at %(date_or_age)s'),
280 ],
280 ],
281 notification.TYPE_PULL_REQUEST_UPDATE: [
281 notification.TYPE_PULL_REQUEST_UPDATE: [
282 _('%(user)s updated pull request %(date_or_age)s'),
282 _('%(user)s updated pull request %(date_or_age)s'),
283 _('%(user)s updated pull request at %(date_or_age)s'),
283 _('%(user)s updated pull request at %(date_or_age)s'),
284 ],
284 ],
285 notification.TYPE_PULL_REQUEST_COMMENT: [
285 notification.TYPE_PULL_REQUEST_COMMENT: [
286 _('%(user)s commented on pull request %(date_or_age)s'),
286 _('%(user)s commented on pull request %(date_or_age)s'),
287 _('%(user)s commented on pull request at %(date_or_age)s'),
287 _('%(user)s commented on pull request at %(date_or_age)s'),
288 ],
288 ],
289 }
289 }
290
290
291 templates = _map[notification.type_]
291 templates = _map[notification.type_]
292
292
293 if show_age:
293 if show_age:
294 template = templates[0]
294 template = templates[0]
295 date_or_age = h.age(notification.created_on)
295 date_or_age = h.age(notification.created_on)
296 if translate:
296 if translate:
297 date_or_age = translate(date_or_age)
297 date_or_age = translate(date_or_age)
298
298
299 if isinstance(date_or_age, TranslationString):
299 if isinstance(date_or_age, TranslationString):
300 date_or_age = date_or_age.interpolate()
300 date_or_age = date_or_age.interpolate()
301
301
302 else:
302 else:
303 template = templates[1]
303 template = templates[1]
304 date_or_age = h.format_date(notification.created_on)
304 date_or_age = h.format_date(notification.created_on)
305
305
306 return template % {
306 return template % {
307 'user': notification.created_by_user.username,
307 'user': notification.created_by_user.username,
308 'date_or_age': date_or_age,
308 'date_or_age': date_or_age,
309 }
309 }
310
310
311
311
312 # Templates for Titles, that could be overwritten by rcextensions
312 # Templates for Titles, that could be overwritten by rcextensions
313 # Title of email for pull-request update
313 # Title of email for pull-request update
314 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
314 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
315 # Title of email for request for pull request review
315 # Title of email for request for pull request review
316 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
316 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
317
317
318 # Title of email for general comment on pull request
318 # Title of email for general comment on pull request
319 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
319 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
320 # Title of email for general comment which includes status change on pull request
320 # Title of email for general comment which includes status change on pull request
321 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
321 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
322 # Title of email for inline comment on a file in pull request
322 # Title of email for inline comment on a file in pull request
323 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
323 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
324
324
325 # Title of email for general comment on commit
325 # Title of email for general comment on commit
326 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
326 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
327 # Title of email for general comment which includes status change on commit
327 # Title of email for general comment which includes status change on commit
328 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
328 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
329 # Title of email for inline comment on a file in commit
329 # Title of email for inline comment on a file in commit
330 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
330 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
331
331
332 import cssutils
332 import cssutils
333 # hijack css utils logger and replace with ours
333 # hijack css utils logger and replace with ours
334 log = logging.getLogger('rhodecode.cssutils.premailer')
334 log = logging.getLogger('rhodecode.cssutils.premailer')
335 cssutils.log.setLog(log)
335 cssutils.log.setLog(log)
336
336
337
337
338 class EmailNotificationModel(BaseModel):
338 class EmailNotificationModel(BaseModel):
339 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
339 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
340 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
340 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
341 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
341 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
342 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
342 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
343 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
343 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
344 TYPE_MAIN = Notification.TYPE_MESSAGE
344 TYPE_MAIN = Notification.TYPE_MESSAGE
345
345
346 TYPE_PASSWORD_RESET = 'password_reset'
346 TYPE_PASSWORD_RESET = 'password_reset'
347 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
347 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
348 TYPE_EMAIL_TEST = 'email_test'
348 TYPE_EMAIL_TEST = 'email_test'
349 TYPE_EMAIL_EXCEPTION = 'exception'
349 TYPE_EMAIL_EXCEPTION = 'exception'
350 TYPE_UPDATE_AVAILABLE = 'update_available'
350 TYPE_UPDATE_AVAILABLE = 'update_available'
351 TYPE_TEST = 'test'
351 TYPE_TEST = 'test'
352
352
353 email_types = {
353 email_types = {
354 TYPE_MAIN:
354 TYPE_MAIN:
355 'rhodecode:templates/email_templates/main.mako',
355 'rhodecode:templates/email_templates/main.mako',
356 TYPE_TEST:
356 TYPE_TEST:
357 'rhodecode:templates/email_templates/test.mako',
357 'rhodecode:templates/email_templates/test.mako',
358 TYPE_EMAIL_EXCEPTION:
358 TYPE_EMAIL_EXCEPTION:
359 'rhodecode:templates/email_templates/exception_tracker.mako',
359 'rhodecode:templates/email_templates/exception_tracker.mako',
360 TYPE_UPDATE_AVAILABLE:
360 TYPE_UPDATE_AVAILABLE:
361 'rhodecode:templates/email_templates/update_available.mako',
361 'rhodecode:templates/email_templates/update_available.mako',
362 TYPE_EMAIL_TEST:
362 TYPE_EMAIL_TEST:
363 'rhodecode:templates/email_templates/email_test.mako',
363 'rhodecode:templates/email_templates/email_test.mako',
364 TYPE_REGISTRATION:
364 TYPE_REGISTRATION:
365 'rhodecode:templates/email_templates/user_registration.mako',
365 'rhodecode:templates/email_templates/user_registration.mako',
366 TYPE_PASSWORD_RESET:
366 TYPE_PASSWORD_RESET:
367 'rhodecode:templates/email_templates/password_reset.mako',
367 'rhodecode:templates/email_templates/password_reset.mako',
368 TYPE_PASSWORD_RESET_CONFIRMATION:
368 TYPE_PASSWORD_RESET_CONFIRMATION:
369 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
369 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
370 TYPE_COMMIT_COMMENT:
370 TYPE_COMMIT_COMMENT:
371 'rhodecode:templates/email_templates/commit_comment.mako',
371 'rhodecode:templates/email_templates/commit_comment.mako',
372 TYPE_PULL_REQUEST:
372 TYPE_PULL_REQUEST:
373 'rhodecode:templates/email_templates/pull_request_review.mako',
373 'rhodecode:templates/email_templates/pull_request_review.mako',
374 TYPE_PULL_REQUEST_COMMENT:
374 TYPE_PULL_REQUEST_COMMENT:
375 'rhodecode:templates/email_templates/pull_request_comment.mako',
375 'rhodecode:templates/email_templates/pull_request_comment.mako',
376 TYPE_PULL_REQUEST_UPDATE:
376 TYPE_PULL_REQUEST_UPDATE:
377 'rhodecode:templates/email_templates/pull_request_update.mako',
377 'rhodecode:templates/email_templates/pull_request_update.mako',
378 }
378 }
379
379
380 premailer_instance = premailer.Premailer()
380 premailer_instance = premailer.Premailer()
381
381
382 def __init__(self):
382 def __init__(self):
383 """
383 """
384 Example usage::
384 Example usage::
385
385
386 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
386 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
387 EmailNotificationModel.TYPE_TEST, **email_kwargs)
387 EmailNotificationModel.TYPE_TEST, **email_kwargs)
388
388
389 """
389 """
390 super(EmailNotificationModel, self).__init__()
390 super(EmailNotificationModel, self).__init__()
391 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
391 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
392
392
393 def _update_kwargs_for_render(self, kwargs):
393 def _update_kwargs_for_render(self, kwargs):
394 """
394 """
395 Inject params required for Mako rendering
395 Inject params required for Mako rendering
396
396
397 :param kwargs:
397 :param kwargs:
398 """
398 """
399
399
400 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
400 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
401 kwargs['rhodecode_version'] = rhodecode.__version__
401 kwargs['rhodecode_version'] = rhodecode.__version__
402 instance_url = h.route_url('home')
402 instance_url = h.route_url('home')
403 _kwargs = {
403 _kwargs = {
404 'instance_url': instance_url,
404 'instance_url': instance_url,
405 'whitespace_filter': self.whitespace_filter,
405 'whitespace_filter': self.whitespace_filter,
406 'email_pr_update_subject_template': EMAIL_PR_UPDATE_SUBJECT_TEMPLATE,
406 'email_pr_update_subject_template': EMAIL_PR_UPDATE_SUBJECT_TEMPLATE,
407 'email_pr_review_subject_template': EMAIL_PR_REVIEW_SUBJECT_TEMPLATE,
407 'email_pr_review_subject_template': EMAIL_PR_REVIEW_SUBJECT_TEMPLATE,
408 'email_pr_comment_subject_template': EMAIL_PR_COMMENT_SUBJECT_TEMPLATE,
408 'email_pr_comment_subject_template': EMAIL_PR_COMMENT_SUBJECT_TEMPLATE,
409 'email_pr_comment_status_change_subject_template': EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
409 'email_pr_comment_status_change_subject_template': EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
410 'email_pr_comment_file_subject_template': EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE,
410 'email_pr_comment_file_subject_template': EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE,
411 'email_comment_subject_template': EMAIL_COMMENT_SUBJECT_TEMPLATE,
411 'email_comment_subject_template': EMAIL_COMMENT_SUBJECT_TEMPLATE,
412 'email_comment_status_change_subject_template': EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
412 'email_comment_status_change_subject_template': EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
413 'email_comment_file_subject_template': EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE,
413 'email_comment_file_subject_template': EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE,
414 }
414 }
415 _kwargs.update(kwargs)
415 _kwargs.update(kwargs)
416 return _kwargs
416 return _kwargs
417
417
418 def whitespace_filter(self, text):
418 def whitespace_filter(self, text):
419 return text.replace('\n', '').replace('\t', '')
419 return text.replace('\n', '').replace('\t', '')
420
420
421 def get_renderer(self, type_, request):
421 def get_renderer(self, type_, request):
422 template_name = self.email_types[type_]
422 template_name = self.email_types[type_]
423 return request.get_partial_renderer(template_name)
423 return request.get_partial_renderer(template_name)
424
424
425 def render_email(self, type_, **kwargs):
425 def render_email(self, type_, **kwargs):
426 """
426 """
427 renders template for email, and returns a tuple of
427 renders template for email, and returns a tuple of
428 (subject, email_headers, email_html_body, email_plaintext_body)
428 (subject, email_headers, email_html_body, email_plaintext_body)
429 """
429 """
430 request = get_current_request()
430 request = get_current_request()
431
431
432 # translator and helpers inject
432 # translator and helpers inject
433 _kwargs = self._update_kwargs_for_render(kwargs)
433 _kwargs = self._update_kwargs_for_render(kwargs)
434 email_template = self.get_renderer(type_, request=request)
434 email_template = self.get_renderer(type_, request=request)
435 subject = email_template.render('subject', **_kwargs)
435 subject = email_template.render('subject', **_kwargs)
436
436
437 try:
437 try:
438 body_plaintext = email_template.render('body_plaintext', **_kwargs)
438 body_plaintext = email_template.render('body_plaintext', **_kwargs)
439 except AttributeError:
439 except AttributeError:
440 # it's not defined in template, ok we can skip it
440 # it's not defined in template, ok we can skip it
441 body_plaintext = ''
441 body_plaintext = ''
442
442
443 # render WHOLE template
443 # render WHOLE template
444 body = email_template.render(None, **_kwargs)
444 body = email_template.render(None, **_kwargs)
445
445
446 try:
446 try:
447 # Inline CSS styles and conversion
447 # Inline CSS styles and conversion
448 body = self.premailer_instance.transform(body)
448 body = self.premailer_instance.transform(body)
449 except Exception:
449 except Exception:
450 log.exception('Failed to parse body with premailer')
450 log.exception('Failed to parse body with premailer')
451 pass
451 pass
452
452
453 return subject, body, body_plaintext
453 return subject, body, body_plaintext
@@ -1,600 +1,599 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 """
20 """
22 permissions model for RhodeCode
21 permissions model for RhodeCode
23 """
22 """
24 import collections
23 import collections
25 import logging
24 import logging
26 import traceback
25 import traceback
27
26
28 from sqlalchemy.exc import DatabaseError
27 from sqlalchemy.exc import DatabaseError
29
28
30 from rhodecode import events
29 from rhodecode import events
31 from rhodecode.model import BaseModel
30 from rhodecode.model import BaseModel
32 from rhodecode.model.db import (
31 from rhodecode.model.db import (
33 User, Permission, UserToPerm, UserRepoToPerm, UserRepoGroupToPerm,
32 User, Permission, UserToPerm, UserRepoToPerm, UserRepoGroupToPerm,
34 UserUserGroupToPerm, UserGroup, UserGroupToPerm, UserToRepoBranchPermission)
33 UserUserGroupToPerm, UserGroup, UserGroupToPerm, UserToRepoBranchPermission)
35 from rhodecode.lib.utils2 import str2bool, safe_int
34 from rhodecode.lib.utils2 import str2bool, safe_int
36
35
37 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
38
37
39
38
40 class PermissionModel(BaseModel):
39 class PermissionModel(BaseModel):
41 """
40 """
42 Permissions model for RhodeCode
41 Permissions model for RhodeCode
43 """
42 """
44 FORKING_DISABLED = 'hg.fork.none'
43 FORKING_DISABLED = 'hg.fork.none'
45 FORKING_ENABLED = 'hg.fork.repository'
44 FORKING_ENABLED = 'hg.fork.repository'
46
45
47 cls = Permission
46 cls = Permission
48 global_perms = {
47 global_perms = {
49 'default_repo_create': None,
48 'default_repo_create': None,
50 # special case for create repos on write access to group
49 # special case for create repos on write access to group
51 'default_repo_create_on_write': None,
50 'default_repo_create_on_write': None,
52 'default_repo_group_create': None,
51 'default_repo_group_create': None,
53 'default_user_group_create': None,
52 'default_user_group_create': None,
54 'default_fork_create': None,
53 'default_fork_create': None,
55 'default_inherit_default_permissions': None,
54 'default_inherit_default_permissions': None,
56 'default_register': None,
55 'default_register': None,
57 'default_password_reset': None,
56 'default_password_reset': None,
58 'default_extern_activate': None,
57 'default_extern_activate': None,
59
58
60 # object permissions below
59 # object permissions below
61 'default_repo_perm': None,
60 'default_repo_perm': None,
62 'default_group_perm': None,
61 'default_group_perm': None,
63 'default_user_group_perm': None,
62 'default_user_group_perm': None,
64
63
65 # branch
64 # branch
66 'default_branch_perm': None,
65 'default_branch_perm': None,
67 }
66 }
68
67
69 def set_global_permission_choices(self, c_obj, gettext_translator):
68 def set_global_permission_choices(self, c_obj, gettext_translator):
70 _ = gettext_translator
69 _ = gettext_translator
71
70
72 c_obj.repo_perms_choices = [
71 c_obj.repo_perms_choices = [
73 ('repository.none', _('None'),),
72 ('repository.none', _('None'),),
74 ('repository.read', _('Read'),),
73 ('repository.read', _('Read'),),
75 ('repository.write', _('Write'),),
74 ('repository.write', _('Write'),),
76 ('repository.admin', _('Admin'),)]
75 ('repository.admin', _('Admin'),)]
77
76
78 c_obj.group_perms_choices = [
77 c_obj.group_perms_choices = [
79 ('group.none', _('None'),),
78 ('group.none', _('None'),),
80 ('group.read', _('Read'),),
79 ('group.read', _('Read'),),
81 ('group.write', _('Write'),),
80 ('group.write', _('Write'),),
82 ('group.admin', _('Admin'),)]
81 ('group.admin', _('Admin'),)]
83
82
84 c_obj.user_group_perms_choices = [
83 c_obj.user_group_perms_choices = [
85 ('usergroup.none', _('None'),),
84 ('usergroup.none', _('None'),),
86 ('usergroup.read', _('Read'),),
85 ('usergroup.read', _('Read'),),
87 ('usergroup.write', _('Write'),),
86 ('usergroup.write', _('Write'),),
88 ('usergroup.admin', _('Admin'),)]
87 ('usergroup.admin', _('Admin'),)]
89
88
90 c_obj.branch_perms_choices = [
89 c_obj.branch_perms_choices = [
91 ('branch.none', _('Protected/No Access'),),
90 ('branch.none', _('Protected/No Access'),),
92 ('branch.merge', _('Web merge'),),
91 ('branch.merge', _('Web merge'),),
93 ('branch.push', _('Push'),),
92 ('branch.push', _('Push'),),
94 ('branch.push_force', _('Force Push'),)]
93 ('branch.push_force', _('Force Push'),)]
95
94
96 c_obj.register_choices = [
95 c_obj.register_choices = [
97 ('hg.register.none', _('Disabled')),
96 ('hg.register.none', _('Disabled')),
98 ('hg.register.manual_activate', _('Allowed with manual account activation')),
97 ('hg.register.manual_activate', _('Allowed with manual account activation')),
99 ('hg.register.auto_activate', _('Allowed with automatic account activation'))]
98 ('hg.register.auto_activate', _('Allowed with automatic account activation'))]
100
99
101 c_obj.password_reset_choices = [
100 c_obj.password_reset_choices = [
102 ('hg.password_reset.enabled', _('Allow password recovery')),
101 ('hg.password_reset.enabled', _('Allow password recovery')),
103 ('hg.password_reset.hidden', _('Hide password recovery link')),
102 ('hg.password_reset.hidden', _('Hide password recovery link')),
104 ('hg.password_reset.disabled', _('Disable password recovery'))]
103 ('hg.password_reset.disabled', _('Disable password recovery'))]
105
104
106 c_obj.extern_activate_choices = [
105 c_obj.extern_activate_choices = [
107 ('hg.extern_activate.manual', _('Manual activation of external account')),
106 ('hg.extern_activate.manual', _('Manual activation of external account')),
108 ('hg.extern_activate.auto', _('Automatic activation of external account'))]
107 ('hg.extern_activate.auto', _('Automatic activation of external account'))]
109
108
110 c_obj.repo_create_choices = [
109 c_obj.repo_create_choices = [
111 ('hg.create.none', _('Disabled')),
110 ('hg.create.none', _('Disabled')),
112 ('hg.create.repository', _('Enabled'))]
111 ('hg.create.repository', _('Enabled'))]
113
112
114 c_obj.repo_create_on_write_choices = [
113 c_obj.repo_create_on_write_choices = [
115 ('hg.create.write_on_repogroup.false', _('Disabled')),
114 ('hg.create.write_on_repogroup.false', _('Disabled')),
116 ('hg.create.write_on_repogroup.true', _('Enabled'))]
115 ('hg.create.write_on_repogroup.true', _('Enabled'))]
117
116
118 c_obj.user_group_create_choices = [
117 c_obj.user_group_create_choices = [
119 ('hg.usergroup.create.false', _('Disabled')),
118 ('hg.usergroup.create.false', _('Disabled')),
120 ('hg.usergroup.create.true', _('Enabled'))]
119 ('hg.usergroup.create.true', _('Enabled'))]
121
120
122 c_obj.repo_group_create_choices = [
121 c_obj.repo_group_create_choices = [
123 ('hg.repogroup.create.false', _('Disabled')),
122 ('hg.repogroup.create.false', _('Disabled')),
124 ('hg.repogroup.create.true', _('Enabled'))]
123 ('hg.repogroup.create.true', _('Enabled'))]
125
124
126 c_obj.fork_choices = [
125 c_obj.fork_choices = [
127 (self.FORKING_DISABLED, _('Disabled')),
126 (self.FORKING_DISABLED, _('Disabled')),
128 (self.FORKING_ENABLED, _('Enabled'))]
127 (self.FORKING_ENABLED, _('Enabled'))]
129
128
130 c_obj.inherit_default_permission_choices = [
129 c_obj.inherit_default_permission_choices = [
131 ('hg.inherit_default_perms.false', _('Disabled')),
130 ('hg.inherit_default_perms.false', _('Disabled')),
132 ('hg.inherit_default_perms.true', _('Enabled'))]
131 ('hg.inherit_default_perms.true', _('Enabled'))]
133
132
134 def get_default_perms(self, object_perms, suffix):
133 def get_default_perms(self, object_perms, suffix):
135 defaults = {}
134 defaults = {}
136 for perm in object_perms:
135 for perm in object_perms:
137 # perms
136 # perms
138 if perm.permission.permission_name.startswith('repository.'):
137 if perm.permission.permission_name.startswith('repository.'):
139 defaults['default_repo_perm' + suffix] = perm.permission.permission_name
138 defaults['default_repo_perm' + suffix] = perm.permission.permission_name
140
139
141 if perm.permission.permission_name.startswith('group.'):
140 if perm.permission.permission_name.startswith('group.'):
142 defaults['default_group_perm' + suffix] = perm.permission.permission_name
141 defaults['default_group_perm' + suffix] = perm.permission.permission_name
143
142
144 if perm.permission.permission_name.startswith('usergroup.'):
143 if perm.permission.permission_name.startswith('usergroup.'):
145 defaults['default_user_group_perm' + suffix] = perm.permission.permission_name
144 defaults['default_user_group_perm' + suffix] = perm.permission.permission_name
146
145
147 # branch
146 # branch
148 if perm.permission.permission_name.startswith('branch.'):
147 if perm.permission.permission_name.startswith('branch.'):
149 defaults['default_branch_perm' + suffix] = perm.permission.permission_name
148 defaults['default_branch_perm' + suffix] = perm.permission.permission_name
150
149
151 # creation of objects
150 # creation of objects
152 if perm.permission.permission_name.startswith('hg.create.write_on_repogroup'):
151 if perm.permission.permission_name.startswith('hg.create.write_on_repogroup'):
153 defaults['default_repo_create_on_write' + suffix] = perm.permission.permission_name
152 defaults['default_repo_create_on_write' + suffix] = perm.permission.permission_name
154
153
155 elif perm.permission.permission_name.startswith('hg.create.'):
154 elif perm.permission.permission_name.startswith('hg.create.'):
156 defaults['default_repo_create' + suffix] = perm.permission.permission_name
155 defaults['default_repo_create' + suffix] = perm.permission.permission_name
157
156
158 if perm.permission.permission_name.startswith('hg.fork.'):
157 if perm.permission.permission_name.startswith('hg.fork.'):
159 defaults['default_fork_create' + suffix] = perm.permission.permission_name
158 defaults['default_fork_create' + suffix] = perm.permission.permission_name
160
159
161 if perm.permission.permission_name.startswith('hg.inherit_default_perms.'):
160 if perm.permission.permission_name.startswith('hg.inherit_default_perms.'):
162 defaults['default_inherit_default_permissions' + suffix] = perm.permission.permission_name
161 defaults['default_inherit_default_permissions' + suffix] = perm.permission.permission_name
163
162
164 if perm.permission.permission_name.startswith('hg.repogroup.'):
163 if perm.permission.permission_name.startswith('hg.repogroup.'):
165 defaults['default_repo_group_create' + suffix] = perm.permission.permission_name
164 defaults['default_repo_group_create' + suffix] = perm.permission.permission_name
166
165
167 if perm.permission.permission_name.startswith('hg.usergroup.'):
166 if perm.permission.permission_name.startswith('hg.usergroup.'):
168 defaults['default_user_group_create' + suffix] = perm.permission.permission_name
167 defaults['default_user_group_create' + suffix] = perm.permission.permission_name
169
168
170 # registration and external account activation
169 # registration and external account activation
171 if perm.permission.permission_name.startswith('hg.register.'):
170 if perm.permission.permission_name.startswith('hg.register.'):
172 defaults['default_register' + suffix] = perm.permission.permission_name
171 defaults['default_register' + suffix] = perm.permission.permission_name
173
172
174 if perm.permission.permission_name.startswith('hg.password_reset.'):
173 if perm.permission.permission_name.startswith('hg.password_reset.'):
175 defaults['default_password_reset' + suffix] = perm.permission.permission_name
174 defaults['default_password_reset' + suffix] = perm.permission.permission_name
176
175
177 if perm.permission.permission_name.startswith('hg.extern_activate.'):
176 if perm.permission.permission_name.startswith('hg.extern_activate.'):
178 defaults['default_extern_activate' + suffix] = perm.permission.permission_name
177 defaults['default_extern_activate' + suffix] = perm.permission.permission_name
179
178
180 return defaults
179 return defaults
181
180
182 def _make_new_user_perm(self, user, perm_name):
181 def _make_new_user_perm(self, user, perm_name):
183 log.debug('Creating new user permission:%s', perm_name)
182 log.debug('Creating new user permission:%s', perm_name)
184 new = UserToPerm()
183 new = UserToPerm()
185 new.user = user
184 new.user = user
186 new.permission = Permission.get_by_key(perm_name)
185 new.permission = Permission.get_by_key(perm_name)
187 return new
186 return new
188
187
189 def _make_new_user_group_perm(self, user_group, perm_name):
188 def _make_new_user_group_perm(self, user_group, perm_name):
190 log.debug('Creating new user group permission:%s', perm_name)
189 log.debug('Creating new user group permission:%s', perm_name)
191 new = UserGroupToPerm()
190 new = UserGroupToPerm()
192 new.users_group = user_group
191 new.users_group = user_group
193 new.permission = Permission.get_by_key(perm_name)
192 new.permission = Permission.get_by_key(perm_name)
194 return new
193 return new
195
194
196 def _keep_perm(self, perm_name, keep_fields):
195 def _keep_perm(self, perm_name, keep_fields):
197 def get_pat(field_name):
196 def get_pat(field_name):
198 return {
197 return {
199 # global perms
198 # global perms
200 'default_repo_create': 'hg.create.',
199 'default_repo_create': 'hg.create.',
201 # special case for create repos on write access to group
200 # special case for create repos on write access to group
202 'default_repo_create_on_write': 'hg.create.write_on_repogroup.',
201 'default_repo_create_on_write': 'hg.create.write_on_repogroup.',
203 'default_repo_group_create': 'hg.repogroup.create.',
202 'default_repo_group_create': 'hg.repogroup.create.',
204 'default_user_group_create': 'hg.usergroup.create.',
203 'default_user_group_create': 'hg.usergroup.create.',
205 'default_fork_create': 'hg.fork.',
204 'default_fork_create': 'hg.fork.',
206 'default_inherit_default_permissions': 'hg.inherit_default_perms.',
205 'default_inherit_default_permissions': 'hg.inherit_default_perms.',
207
206
208 # application perms
207 # application perms
209 'default_register': 'hg.register.',
208 'default_register': 'hg.register.',
210 'default_password_reset': 'hg.password_reset.',
209 'default_password_reset': 'hg.password_reset.',
211 'default_extern_activate': 'hg.extern_activate.',
210 'default_extern_activate': 'hg.extern_activate.',
212
211
213 # object permissions below
212 # object permissions below
214 'default_repo_perm': 'repository.',
213 'default_repo_perm': 'repository.',
215 'default_group_perm': 'group.',
214 'default_group_perm': 'group.',
216 'default_user_group_perm': 'usergroup.',
215 'default_user_group_perm': 'usergroup.',
217 # branch
216 # branch
218 'default_branch_perm': 'branch.',
217 'default_branch_perm': 'branch.',
219
218
220 }[field_name]
219 }[field_name]
221 for field in keep_fields:
220 for field in keep_fields:
222 pat = get_pat(field)
221 pat = get_pat(field)
223 if perm_name.startswith(pat):
222 if perm_name.startswith(pat):
224 return True
223 return True
225 return False
224 return False
226
225
227 def _clear_object_perm(self, object_perms, preserve=None):
226 def _clear_object_perm(self, object_perms, preserve=None):
228 preserve = preserve or []
227 preserve = preserve or []
229 _deleted = []
228 _deleted = []
230 for perm in object_perms:
229 for perm in object_perms:
231 perm_name = perm.permission.permission_name
230 perm_name = perm.permission.permission_name
232 if not self._keep_perm(perm_name, keep_fields=preserve):
231 if not self._keep_perm(perm_name, keep_fields=preserve):
233 _deleted.append(perm_name)
232 _deleted.append(perm_name)
234 self.sa.delete(perm)
233 self.sa.delete(perm)
235 return _deleted
234 return _deleted
236
235
237 def _clear_user_perms(self, user_id, preserve=None):
236 def _clear_user_perms(self, user_id, preserve=None):
238 perms = self.sa.query(UserToPerm)\
237 perms = self.sa.query(UserToPerm)\
239 .filter(UserToPerm.user_id == user_id)\
238 .filter(UserToPerm.user_id == user_id)\
240 .all()
239 .all()
241 return self._clear_object_perm(perms, preserve=preserve)
240 return self._clear_object_perm(perms, preserve=preserve)
242
241
243 def _clear_user_group_perms(self, user_group_id, preserve=None):
242 def _clear_user_group_perms(self, user_group_id, preserve=None):
244 perms = self.sa.query(UserGroupToPerm)\
243 perms = self.sa.query(UserGroupToPerm)\
245 .filter(UserGroupToPerm.users_group_id == user_group_id)\
244 .filter(UserGroupToPerm.users_group_id == user_group_id)\
246 .all()
245 .all()
247 return self._clear_object_perm(perms, preserve=preserve)
246 return self._clear_object_perm(perms, preserve=preserve)
248
247
249 def _set_new_object_perms(self, obj_type, to_object, form_result, preserve=None):
248 def _set_new_object_perms(self, obj_type, to_object, form_result, preserve=None):
250 # clear current entries, to make this function idempotent
249 # clear current entries, to make this function idempotent
251 # it will fix even if we define more permissions or permissions
250 # it will fix even if we define more permissions or permissions
252 # are somehow missing
251 # are somehow missing
253 preserve = preserve or []
252 preserve = preserve or []
254 _global_perms = self.global_perms.copy()
253 _global_perms = self.global_perms.copy()
255 if obj_type not in ['user', 'user_group']:
254 if obj_type not in ['user', 'user_group']:
256 raise ValueError("obj_type must be on of 'user' or 'user_group'")
255 raise ValueError("obj_type must be on of 'user' or 'user_group'")
257 global_perms = len(_global_perms)
256 global_perms = len(_global_perms)
258 default_user_perms = len(Permission.DEFAULT_USER_PERMISSIONS)
257 default_user_perms = len(Permission.DEFAULT_USER_PERMISSIONS)
259 if global_perms != default_user_perms:
258 if global_perms != default_user_perms:
260 raise Exception(
259 raise Exception(
261 'Inconsistent permissions definition. Got {} vs {}'.format(
260 'Inconsistent permissions definition. Got {} vs {}'.format(
262 global_perms, default_user_perms))
261 global_perms, default_user_perms))
263
262
264 if obj_type == 'user':
263 if obj_type == 'user':
265 self._clear_user_perms(to_object.user_id, preserve)
264 self._clear_user_perms(to_object.user_id, preserve)
266 if obj_type == 'user_group':
265 if obj_type == 'user_group':
267 self._clear_user_group_perms(to_object.users_group_id, preserve)
266 self._clear_user_group_perms(to_object.users_group_id, preserve)
268
267
269 # now kill the keys that we want to preserve from the form.
268 # now kill the keys that we want to preserve from the form.
270 for key in preserve:
269 for key in preserve:
271 del _global_perms[key]
270 del _global_perms[key]
272
271
273 for k in _global_perms.copy():
272 for k in _global_perms.copy():
274 _global_perms[k] = form_result[k]
273 _global_perms[k] = form_result[k]
275
274
276 # at that stage we validate all are passed inside form_result
275 # at that stage we validate all are passed inside form_result
277 for _perm_key, perm_value in _global_perms.items():
276 for _perm_key, perm_value in _global_perms.items():
278 if perm_value is None:
277 if perm_value is None:
279 raise ValueError('Missing permission for %s' % (_perm_key,))
278 raise ValueError('Missing permission for %s' % (_perm_key,))
280
279
281 if obj_type == 'user':
280 if obj_type == 'user':
282 p = self._make_new_user_perm(object, perm_value)
281 p = self._make_new_user_perm(object, perm_value)
283 self.sa.add(p)
282 self.sa.add(p)
284 if obj_type == 'user_group':
283 if obj_type == 'user_group':
285 p = self._make_new_user_group_perm(object, perm_value)
284 p = self._make_new_user_group_perm(object, perm_value)
286 self.sa.add(p)
285 self.sa.add(p)
287
286
288 def _set_new_user_perms(self, user, form_result, preserve=None):
287 def _set_new_user_perms(self, user, form_result, preserve=None):
289 return self._set_new_object_perms(
288 return self._set_new_object_perms(
290 'user', user, form_result, preserve)
289 'user', user, form_result, preserve)
291
290
292 def _set_new_user_group_perms(self, user_group, form_result, preserve=None):
291 def _set_new_user_group_perms(self, user_group, form_result, preserve=None):
293 return self._set_new_object_perms(
292 return self._set_new_object_perms(
294 'user_group', user_group, form_result, preserve)
293 'user_group', user_group, form_result, preserve)
295
294
296 def set_new_user_perms(self, user, form_result):
295 def set_new_user_perms(self, user, form_result):
297 # calculate what to preserve from what is given in form_result
296 # calculate what to preserve from what is given in form_result
298 preserve = set(self.global_perms.keys()).difference(set(form_result.keys()))
297 preserve = set(self.global_perms.keys()).difference(set(form_result.keys()))
299 return self._set_new_user_perms(user, form_result, preserve)
298 return self._set_new_user_perms(user, form_result, preserve)
300
299
301 def set_new_user_group_perms(self, user_group, form_result):
300 def set_new_user_group_perms(self, user_group, form_result):
302 # calculate what to preserve from what is given in form_result
301 # calculate what to preserve from what is given in form_result
303 preserve = set(self.global_perms.keys()).difference(set(form_result.keys()))
302 preserve = set(self.global_perms.keys()).difference(set(form_result.keys()))
304 return self._set_new_user_group_perms(user_group, form_result, preserve)
303 return self._set_new_user_group_perms(user_group, form_result, preserve)
305
304
306 def create_permissions(self):
305 def create_permissions(self):
307 """
306 """
308 Create permissions for whole system
307 Create permissions for whole system
309 """
308 """
310 for p in Permission.PERMS:
309 for p in Permission.PERMS:
311 if not Permission.get_by_key(p[0]):
310 if not Permission.get_by_key(p[0]):
312 new_perm = Permission()
311 new_perm = Permission()
313 new_perm.permission_name = p[0]
312 new_perm.permission_name = p[0]
314 new_perm.permission_longname = p[0] # translation err with p[1]
313 new_perm.permission_longname = p[0] # translation err with p[1]
315 self.sa.add(new_perm)
314 self.sa.add(new_perm)
316
315
317 def _create_default_object_permission(self, obj_type, obj, obj_perms,
316 def _create_default_object_permission(self, obj_type, obj, obj_perms,
318 force=False):
317 force=False):
319 if obj_type not in ['user', 'user_group']:
318 if obj_type not in ['user', 'user_group']:
320 raise ValueError("obj_type must be on of 'user' or 'user_group'")
319 raise ValueError("obj_type must be on of 'user' or 'user_group'")
321
320
322 def _get_group(perm_name):
321 def _get_group(perm_name):
323 return '.'.join(perm_name.split('.')[:1])
322 return '.'.join(perm_name.split('.')[:1])
324
323
325 defined_perms_groups = map(
324 defined_perms_groups = map(
326 _get_group, (x.permission.permission_name for x in obj_perms))
325 _get_group, (x.permission.permission_name for x in obj_perms))
327 log.debug('GOT ALREADY DEFINED:%s', obj_perms)
326 log.debug('GOT ALREADY DEFINED:%s', obj_perms)
328
327
329 if force:
328 if force:
330 self._clear_object_perm(obj_perms)
329 self._clear_object_perm(obj_perms)
331 self.sa.commit()
330 self.sa.commit()
332 defined_perms_groups = []
331 defined_perms_groups = []
333 # for every default permission that needs to be created, we check if
332 # for every default permission that needs to be created, we check if
334 # it's group is already defined, if it's not we create default perm
333 # it's group is already defined, if it's not we create default perm
335 for perm_name in Permission.DEFAULT_USER_PERMISSIONS:
334 for perm_name in Permission.DEFAULT_USER_PERMISSIONS:
336 gr = _get_group(perm_name)
335 gr = _get_group(perm_name)
337 if gr not in defined_perms_groups:
336 if gr not in defined_perms_groups:
338 log.debug('GR:%s not found, creating permission %s',
337 log.debug('GR:%s not found, creating permission %s',
339 gr, perm_name)
338 gr, perm_name)
340 if obj_type == 'user':
339 if obj_type == 'user':
341 new_perm = self._make_new_user_perm(obj, perm_name)
340 new_perm = self._make_new_user_perm(obj, perm_name)
342 self.sa.add(new_perm)
341 self.sa.add(new_perm)
343 if obj_type == 'user_group':
342 if obj_type == 'user_group':
344 new_perm = self._make_new_user_group_perm(obj, perm_name)
343 new_perm = self._make_new_user_group_perm(obj, perm_name)
345 self.sa.add(new_perm)
344 self.sa.add(new_perm)
346
345
347 def create_default_user_permissions(self, user, force=False):
346 def create_default_user_permissions(self, user, force=False):
348 """
347 """
349 Creates only missing default permissions for user, if force is set it
348 Creates only missing default permissions for user, if force is set it
350 resets the default permissions for that user
349 resets the default permissions for that user
351
350
352 :param user:
351 :param user:
353 :param force:
352 :param force:
354 """
353 """
355 user = self._get_user(user)
354 user = self._get_user(user)
356 obj_perms = UserToPerm.query().filter(UserToPerm.user == user).all()
355 obj_perms = UserToPerm.query().filter(UserToPerm.user == user).all()
357 return self._create_default_object_permission(
356 return self._create_default_object_permission(
358 'user', user, obj_perms, force)
357 'user', user, obj_perms, force)
359
358
360 def create_default_user_group_permissions(self, user_group, force=False):
359 def create_default_user_group_permissions(self, user_group, force=False):
361 """
360 """
362 Creates only missing default permissions for user group, if force is
361 Creates only missing default permissions for user group, if force is
363 set it resets the default permissions for that user group
362 set it resets the default permissions for that user group
364
363
365 :param user_group:
364 :param user_group:
366 :param force:
365 :param force:
367 """
366 """
368 user_group = self._get_user_group(user_group)
367 user_group = self._get_user_group(user_group)
369 obj_perms = UserToPerm.query().filter(UserGroupToPerm.users_group == user_group).all()
368 obj_perms = UserToPerm.query().filter(UserGroupToPerm.users_group == user_group).all()
370 return self._create_default_object_permission(
369 return self._create_default_object_permission(
371 'user_group', user_group, obj_perms, force)
370 'user_group', user_group, obj_perms, force)
372
371
373 def update_application_permissions(self, form_result):
372 def update_application_permissions(self, form_result):
374 if 'perm_user_id' in form_result:
373 if 'perm_user_id' in form_result:
375 perm_user = User.get(safe_int(form_result['perm_user_id']))
374 perm_user = User.get(safe_int(form_result['perm_user_id']))
376 else:
375 else:
377 # used mostly to do lookup for default user
376 # used mostly to do lookup for default user
378 perm_user = User.get_by_username(form_result['perm_user_name'])
377 perm_user = User.get_by_username(form_result['perm_user_name'])
379
378
380 try:
379 try:
381 # stage 1 set anonymous access
380 # stage 1 set anonymous access
382 if perm_user.username == User.DEFAULT_USER:
381 if perm_user.username == User.DEFAULT_USER:
383 perm_user.active = str2bool(form_result['anonymous'])
382 perm_user.active = str2bool(form_result['anonymous'])
384 self.sa.add(perm_user)
383 self.sa.add(perm_user)
385
384
386 # stage 2 reset defaults and set them from form data
385 # stage 2 reset defaults and set them from form data
387 self._set_new_user_perms(perm_user, form_result, preserve=[
386 self._set_new_user_perms(perm_user, form_result, preserve=[
388 'default_repo_perm',
387 'default_repo_perm',
389 'default_group_perm',
388 'default_group_perm',
390 'default_user_group_perm',
389 'default_user_group_perm',
391 'default_branch_perm',
390 'default_branch_perm',
392
391
393 'default_repo_group_create',
392 'default_repo_group_create',
394 'default_user_group_create',
393 'default_user_group_create',
395 'default_repo_create_on_write',
394 'default_repo_create_on_write',
396 'default_repo_create',
395 'default_repo_create',
397 'default_fork_create',
396 'default_fork_create',
398 'default_inherit_default_permissions'])
397 'default_inherit_default_permissions'])
399
398
400 self.sa.commit()
399 self.sa.commit()
401 except (DatabaseError,):
400 except (DatabaseError,):
402 log.error(traceback.format_exc())
401 log.error(traceback.format_exc())
403 self.sa.rollback()
402 self.sa.rollback()
404 raise
403 raise
405
404
406 def update_user_permissions(self, form_result):
405 def update_user_permissions(self, form_result):
407 if 'perm_user_id' in form_result:
406 if 'perm_user_id' in form_result:
408 perm_user = User.get(safe_int(form_result['perm_user_id']))
407 perm_user = User.get(safe_int(form_result['perm_user_id']))
409 else:
408 else:
410 # used mostly to do lookup for default user
409 # used mostly to do lookup for default user
411 perm_user = User.get_by_username(form_result['perm_user_name'])
410 perm_user = User.get_by_username(form_result['perm_user_name'])
412 try:
411 try:
413 # stage 2 reset defaults and set them from form data
412 # stage 2 reset defaults and set them from form data
414 self._set_new_user_perms(perm_user, form_result, preserve=[
413 self._set_new_user_perms(perm_user, form_result, preserve=[
415 'default_repo_perm',
414 'default_repo_perm',
416 'default_group_perm',
415 'default_group_perm',
417 'default_user_group_perm',
416 'default_user_group_perm',
418 'default_branch_perm',
417 'default_branch_perm',
419
418
420 'default_register',
419 'default_register',
421 'default_password_reset',
420 'default_password_reset',
422 'default_extern_activate'])
421 'default_extern_activate'])
423 self.sa.commit()
422 self.sa.commit()
424 except (DatabaseError,):
423 except (DatabaseError,):
425 log.error(traceback.format_exc())
424 log.error(traceback.format_exc())
426 self.sa.rollback()
425 self.sa.rollback()
427 raise
426 raise
428
427
429 def update_user_group_permissions(self, form_result):
428 def update_user_group_permissions(self, form_result):
430 if 'perm_user_group_id' in form_result:
429 if 'perm_user_group_id' in form_result:
431 perm_user_group = UserGroup.get(safe_int(form_result['perm_user_group_id']))
430 perm_user_group = UserGroup.get(safe_int(form_result['perm_user_group_id']))
432 else:
431 else:
433 # used mostly to do lookup for default user
432 # used mostly to do lookup for default user
434 perm_user_group = UserGroup.get_by_group_name(form_result['perm_user_group_name'])
433 perm_user_group = UserGroup.get_by_group_name(form_result['perm_user_group_name'])
435 try:
434 try:
436 # stage 2 reset defaults and set them from form data
435 # stage 2 reset defaults and set them from form data
437 self._set_new_user_group_perms(perm_user_group, form_result, preserve=[
436 self._set_new_user_group_perms(perm_user_group, form_result, preserve=[
438 'default_repo_perm',
437 'default_repo_perm',
439 'default_group_perm',
438 'default_group_perm',
440 'default_user_group_perm',
439 'default_user_group_perm',
441 'default_branch_perm',
440 'default_branch_perm',
442
441
443 'default_register',
442 'default_register',
444 'default_password_reset',
443 'default_password_reset',
445 'default_extern_activate'])
444 'default_extern_activate'])
446 self.sa.commit()
445 self.sa.commit()
447 except (DatabaseError,):
446 except (DatabaseError,):
448 log.error(traceback.format_exc())
447 log.error(traceback.format_exc())
449 self.sa.rollback()
448 self.sa.rollback()
450 raise
449 raise
451
450
452 def update_object_permissions(self, form_result):
451 def update_object_permissions(self, form_result):
453 if 'perm_user_id' in form_result:
452 if 'perm_user_id' in form_result:
454 perm_user = User.get(safe_int(form_result['perm_user_id']))
453 perm_user = User.get(safe_int(form_result['perm_user_id']))
455 else:
454 else:
456 # used mostly to do lookup for default user
455 # used mostly to do lookup for default user
457 perm_user = User.get_by_username(form_result['perm_user_name'])
456 perm_user = User.get_by_username(form_result['perm_user_name'])
458 try:
457 try:
459
458
460 # stage 2 reset defaults and set them from form data
459 # stage 2 reset defaults and set them from form data
461 self._set_new_user_perms(perm_user, form_result, preserve=[
460 self._set_new_user_perms(perm_user, form_result, preserve=[
462 'default_repo_group_create',
461 'default_repo_group_create',
463 'default_user_group_create',
462 'default_user_group_create',
464 'default_repo_create_on_write',
463 'default_repo_create_on_write',
465 'default_repo_create',
464 'default_repo_create',
466 'default_fork_create',
465 'default_fork_create',
467 'default_inherit_default_permissions',
466 'default_inherit_default_permissions',
468 'default_branch_perm',
467 'default_branch_perm',
469
468
470 'default_register',
469 'default_register',
471 'default_password_reset',
470 'default_password_reset',
472 'default_extern_activate'])
471 'default_extern_activate'])
473
472
474 # overwrite default repo permissions
473 # overwrite default repo permissions
475 if form_result['overwrite_default_repo']:
474 if form_result['overwrite_default_repo']:
476 _def_name = form_result['default_repo_perm'].split('repository.')[-1]
475 _def_name = form_result['default_repo_perm'].split('repository.')[-1]
477 _def = Permission.get_by_key('repository.' + _def_name)
476 _def = Permission.get_by_key('repository.' + _def_name)
478 for r2p in self.sa.query(UserRepoToPerm)\
477 for r2p in self.sa.query(UserRepoToPerm)\
479 .filter(UserRepoToPerm.user == perm_user)\
478 .filter(UserRepoToPerm.user == perm_user)\
480 .all():
479 .all():
481 # don't reset PRIVATE repositories
480 # don't reset PRIVATE repositories
482 if not r2p.repository.private:
481 if not r2p.repository.private:
483 r2p.permission = _def
482 r2p.permission = _def
484 self.sa.add(r2p)
483 self.sa.add(r2p)
485
484
486 # overwrite default repo group permissions
485 # overwrite default repo group permissions
487 if form_result['overwrite_default_group']:
486 if form_result['overwrite_default_group']:
488 _def_name = form_result['default_group_perm'].split('group.')[-1]
487 _def_name = form_result['default_group_perm'].split('group.')[-1]
489 _def = Permission.get_by_key('group.' + _def_name)
488 _def = Permission.get_by_key('group.' + _def_name)
490 for g2p in self.sa.query(UserRepoGroupToPerm)\
489 for g2p in self.sa.query(UserRepoGroupToPerm)\
491 .filter(UserRepoGroupToPerm.user == perm_user)\
490 .filter(UserRepoGroupToPerm.user == perm_user)\
492 .all():
491 .all():
493 g2p.permission = _def
492 g2p.permission = _def
494 self.sa.add(g2p)
493 self.sa.add(g2p)
495
494
496 # overwrite default user group permissions
495 # overwrite default user group permissions
497 if form_result['overwrite_default_user_group']:
496 if form_result['overwrite_default_user_group']:
498 _def_name = form_result['default_user_group_perm'].split('usergroup.')[-1]
497 _def_name = form_result['default_user_group_perm'].split('usergroup.')[-1]
499 # user groups
498 # user groups
500 _def = Permission.get_by_key('usergroup.' + _def_name)
499 _def = Permission.get_by_key('usergroup.' + _def_name)
501 for g2p in self.sa.query(UserUserGroupToPerm)\
500 for g2p in self.sa.query(UserUserGroupToPerm)\
502 .filter(UserUserGroupToPerm.user == perm_user)\
501 .filter(UserUserGroupToPerm.user == perm_user)\
503 .all():
502 .all():
504 g2p.permission = _def
503 g2p.permission = _def
505 self.sa.add(g2p)
504 self.sa.add(g2p)
506
505
507 # COMMIT
506 # COMMIT
508 self.sa.commit()
507 self.sa.commit()
509 except (DatabaseError,):
508 except (DatabaseError,):
510 log.exception('Failed to set default object permissions')
509 log.exception('Failed to set default object permissions')
511 self.sa.rollback()
510 self.sa.rollback()
512 raise
511 raise
513
512
514 def update_branch_permissions(self, form_result):
513 def update_branch_permissions(self, form_result):
515 if 'perm_user_id' in form_result:
514 if 'perm_user_id' in form_result:
516 perm_user = User.get(safe_int(form_result['perm_user_id']))
515 perm_user = User.get(safe_int(form_result['perm_user_id']))
517 else:
516 else:
518 # used mostly to do lookup for default user
517 # used mostly to do lookup for default user
519 perm_user = User.get_by_username(form_result['perm_user_name'])
518 perm_user = User.get_by_username(form_result['perm_user_name'])
520 try:
519 try:
521
520
522 # stage 2 reset defaults and set them from form data
521 # stage 2 reset defaults and set them from form data
523 self._set_new_user_perms(perm_user, form_result, preserve=[
522 self._set_new_user_perms(perm_user, form_result, preserve=[
524 'default_repo_perm',
523 'default_repo_perm',
525 'default_group_perm',
524 'default_group_perm',
526 'default_user_group_perm',
525 'default_user_group_perm',
527
526
528 'default_repo_group_create',
527 'default_repo_group_create',
529 'default_user_group_create',
528 'default_user_group_create',
530 'default_repo_create_on_write',
529 'default_repo_create_on_write',
531 'default_repo_create',
530 'default_repo_create',
532 'default_fork_create',
531 'default_fork_create',
533 'default_inherit_default_permissions',
532 'default_inherit_default_permissions',
534
533
535 'default_register',
534 'default_register',
536 'default_password_reset',
535 'default_password_reset',
537 'default_extern_activate'])
536 'default_extern_activate'])
538
537
539 # overwrite default branch permissions
538 # overwrite default branch permissions
540 if form_result['overwrite_default_branch']:
539 if form_result['overwrite_default_branch']:
541 _def_name = \
540 _def_name = \
542 form_result['default_branch_perm'].split('branch.')[-1]
541 form_result['default_branch_perm'].split('branch.')[-1]
543
542
544 _def = Permission.get_by_key('branch.' + _def_name)
543 _def = Permission.get_by_key('branch.' + _def_name)
545
544
546 user_perms = UserToRepoBranchPermission.query()\
545 user_perms = UserToRepoBranchPermission.query()\
547 .join(UserToRepoBranchPermission.user_repo_to_perm)\
546 .join(UserToRepoBranchPermission.user_repo_to_perm)\
548 .filter(UserRepoToPerm.user == perm_user).all()
547 .filter(UserRepoToPerm.user == perm_user).all()
549
548
550 for g2p in user_perms:
549 for g2p in user_perms:
551 g2p.permission = _def
550 g2p.permission = _def
552 self.sa.add(g2p)
551 self.sa.add(g2p)
553
552
554 # COMMIT
553 # COMMIT
555 self.sa.commit()
554 self.sa.commit()
556 except (DatabaseError,):
555 except (DatabaseError,):
557 log.exception('Failed to set default branch permissions')
556 log.exception('Failed to set default branch permissions')
558 self.sa.rollback()
557 self.sa.rollback()
559 raise
558 raise
560
559
561 def get_users_with_repo_write(self, db_repo):
560 def get_users_with_repo_write(self, db_repo):
562 write_plus = ['repository.write', 'repository.admin']
561 write_plus = ['repository.write', 'repository.admin']
563 default_user_id = User.get_default_user_id()
562 default_user_id = User.get_default_user_id()
564 user_write_permissions = collections.OrderedDict()
563 user_write_permissions = collections.OrderedDict()
565
564
566 # write or higher and DEFAULT user for inheritance
565 # write or higher and DEFAULT user for inheritance
567 for perm in db_repo.permissions():
566 for perm in db_repo.permissions():
568 if perm.permission in write_plus or perm.user_id == default_user_id:
567 if perm.permission in write_plus or perm.user_id == default_user_id:
569 user_write_permissions[perm.user_id] = perm
568 user_write_permissions[perm.user_id] = perm
570 return user_write_permissions
569 return user_write_permissions
571
570
572 def get_user_groups_with_repo_write(self, db_repo):
571 def get_user_groups_with_repo_write(self, db_repo):
573 write_plus = ['repository.write', 'repository.admin']
572 write_plus = ['repository.write', 'repository.admin']
574 user_group_write_permissions = collections.OrderedDict()
573 user_group_write_permissions = collections.OrderedDict()
575
574
576 # write or higher and DEFAULT user for inheritance
575 # write or higher and DEFAULT user for inheritance
577 for p in db_repo.permission_user_groups():
576 for p in db_repo.permission_user_groups():
578 if p.permission in write_plus:
577 if p.permission in write_plus:
579 user_group_write_permissions[p.users_group_id] = p
578 user_group_write_permissions[p.users_group_id] = p
580 return user_group_write_permissions
579 return user_group_write_permissions
581
580
582 def trigger_permission_flush(self, affected_user_ids=None):
581 def trigger_permission_flush(self, affected_user_ids=None):
583 affected_user_ids = affected_user_ids or User.get_all_user_ids()
582 affected_user_ids = affected_user_ids or User.get_all_user_ids()
584 events.trigger(events.UserPermissionsChange(affected_user_ids))
583 events.trigger(events.UserPermissionsChange(affected_user_ids))
585
584
586 def flush_user_permission_caches(self, changes, affected_user_ids=None):
585 def flush_user_permission_caches(self, changes, affected_user_ids=None):
587 affected_user_ids = affected_user_ids or []
586 affected_user_ids = affected_user_ids or []
588
587
589 for change in changes['added'] + changes['updated'] + changes['deleted']:
588 for change in changes['added'] + changes['updated'] + changes['deleted']:
590 if change['type'] == 'user':
589 if change['type'] == 'user':
591 affected_user_ids.append(change['id'])
590 affected_user_ids.append(change['id'])
592 if change['type'] == 'user_group':
591 if change['type'] == 'user_group':
593 user_group = UserGroup.get(safe_int(change['id']))
592 user_group = UserGroup.get(safe_int(change['id']))
594 if user_group:
593 if user_group:
595 group_members_ids = [x.user_id for x in user_group.members]
594 group_members_ids = [x.user_id for x in user_group.members]
596 affected_user_ids.extend(group_members_ids)
595 affected_user_ids.extend(group_members_ids)
597
596
598 self.trigger_permission_flush(affected_user_ids)
597 self.trigger_permission_flush(affected_user_ids)
599
598
600 return affected_user_ids
599 return affected_user_ids
@@ -1,2380 +1,2380 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib.request, urllib.parse, urllib.error
32 import urllib.request, urllib.parse, urllib.error
33 import collections
33 import collections
34
34
35 from pyramid.threadlocal import get_current_request
35 from pyramid.threadlocal import get_current_request
36
36
37 from rhodecode.lib.vcs.nodes import FileNode
37 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.translation import lazy_ugettext
38 from rhodecode.translation import lazy_ugettext
39 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import audit_logger
40 from rhodecode.lib import audit_logger
41 from collections import OrderedDict
41 from collections import OrderedDict
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.markup_renderer import (
43 from rhodecode.lib.markup_renderer import (
44 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 from rhodecode.lib.utils2 import (
45 from rhodecode.lib.utils2 import (
46 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
46 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
47 get_current_rhodecode_user)
47 get_current_rhodecode_user)
48 from rhodecode.lib.vcs.backends.base import (
48 from rhodecode.lib.vcs.backends.base import (
49 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
49 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 TargetRefMissing, SourceRefMissing)
50 TargetRefMissing, SourceRefMissing)
51 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.exceptions import (
52 from rhodecode.lib.vcs.exceptions import (
53 CommitDoesNotExistError, EmptyRepositoryError)
53 CommitDoesNotExistError, EmptyRepositoryError)
54 from rhodecode.model import BaseModel
54 from rhodecode.model import BaseModel
55 from rhodecode.model.changeset_status import ChangesetStatusModel
55 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.comment import CommentsModel
56 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.db import (
57 from rhodecode.model.db import (
58 aliased, null, lazyload, and_, or_, func, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
58 aliased, null, lazyload, and_, or_, func, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
59 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 from rhodecode.model.meta import Session
60 from rhodecode.model.meta import Session
61 from rhodecode.model.notification import NotificationModel, \
61 from rhodecode.model.notification import NotificationModel, \
62 EmailNotificationModel
62 EmailNotificationModel
63 from rhodecode.model.scm import ScmModel
63 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.settings import VcsSettingsModel
64 from rhodecode.model.settings import VcsSettingsModel
65
65
66
66
67 log = logging.getLogger(__name__)
67 log = logging.getLogger(__name__)
68
68
69
69
70 # Data structure to hold the response data when updating commits during a pull
70 # Data structure to hold the response data when updating commits during a pull
71 # request update.
71 # request update.
72 class UpdateResponse(object):
72 class UpdateResponse(object):
73
73
74 def __init__(self, executed, reason, new, old, common_ancestor_id,
74 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 commit_changes, source_changed, target_changed):
75 commit_changes, source_changed, target_changed):
76
76
77 self.executed = executed
77 self.executed = executed
78 self.reason = reason
78 self.reason = reason
79 self.new = new
79 self.new = new
80 self.old = old
80 self.old = old
81 self.common_ancestor_id = common_ancestor_id
81 self.common_ancestor_id = common_ancestor_id
82 self.changes = commit_changes
82 self.changes = commit_changes
83 self.source_changed = source_changed
83 self.source_changed = source_changed
84 self.target_changed = target_changed
84 self.target_changed = target_changed
85
85
86
86
87 def get_diff_info(
87 def get_diff_info(
88 source_repo, source_ref, target_repo, target_ref, get_authors=False,
88 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 get_commit_authors=True):
89 get_commit_authors=True):
90 """
90 """
91 Calculates detailed diff information for usage in preview of creation of a pull-request.
91 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 This is also used for default reviewers logic
92 This is also used for default reviewers logic
93 """
93 """
94
94
95 source_scm = source_repo.scm_instance()
95 source_scm = source_repo.scm_instance()
96 target_scm = target_repo.scm_instance()
96 target_scm = target_repo.scm_instance()
97
97
98 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
98 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 if not ancestor_id:
99 if not ancestor_id:
100 raise ValueError(
100 raise ValueError(
101 'cannot calculate diff info without a common ancestor. '
101 'cannot calculate diff info without a common ancestor. '
102 'Make sure both repositories are related, and have a common forking commit.')
102 'Make sure both repositories are related, and have a common forking commit.')
103
103
104 # case here is that want a simple diff without incoming commits,
104 # case here is that want a simple diff without incoming commits,
105 # previewing what will be merged based only on commits in the source.
105 # previewing what will be merged based only on commits in the source.
106 log.debug('Using ancestor %s as source_ref instead of %s',
106 log.debug('Using ancestor %s as source_ref instead of %s',
107 ancestor_id, source_ref)
107 ancestor_id, source_ref)
108
108
109 # source of changes now is the common ancestor
109 # source of changes now is the common ancestor
110 source_commit = source_scm.get_commit(commit_id=ancestor_id)
110 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 # target commit becomes the source ref as it is the last commit
111 # target commit becomes the source ref as it is the last commit
112 # for diff generation this logic gives proper diff
112 # for diff generation this logic gives proper diff
113 target_commit = source_scm.get_commit(commit_id=source_ref)
113 target_commit = source_scm.get_commit(commit_id=source_ref)
114
114
115 vcs_diff = \
115 vcs_diff = \
116 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
116 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 ignore_whitespace=False, context=3)
117 ignore_whitespace=False, context=3)
118
118
119 diff_processor = diffs.DiffProcessor(
119 diff_processor = diffs.DiffProcessor(
120 vcs_diff, format='newdiff', diff_limit=None,
120 vcs_diff, format='newdiff', diff_limit=None,
121 file_limit=None, show_full_diff=True)
121 file_limit=None, show_full_diff=True)
122
122
123 _parsed = diff_processor.prepare()
123 _parsed = diff_processor.prepare()
124
124
125 all_files = []
125 all_files = []
126 all_files_changes = []
126 all_files_changes = []
127 changed_lines = {}
127 changed_lines = {}
128 stats = [0, 0]
128 stats = [0, 0]
129 for f in _parsed:
129 for f in _parsed:
130 all_files.append(f['filename'])
130 all_files.append(f['filename'])
131 all_files_changes.append({
131 all_files_changes.append({
132 'filename': f['filename'],
132 'filename': f['filename'],
133 'stats': f['stats']
133 'stats': f['stats']
134 })
134 })
135 stats[0] += f['stats']['added']
135 stats[0] += f['stats']['added']
136 stats[1] += f['stats']['deleted']
136 stats[1] += f['stats']['deleted']
137
137
138 changed_lines[f['filename']] = []
138 changed_lines[f['filename']] = []
139 if len(f['chunks']) < 2:
139 if len(f['chunks']) < 2:
140 continue
140 continue
141 # first line is "context" information
141 # first line is "context" information
142 for chunks in f['chunks'][1:]:
142 for chunks in f['chunks'][1:]:
143 for chunk in chunks['lines']:
143 for chunk in chunks['lines']:
144 if chunk['action'] not in ('del', 'mod'):
144 if chunk['action'] not in ('del', 'mod'):
145 continue
145 continue
146 changed_lines[f['filename']].append(chunk['old_lineno'])
146 changed_lines[f['filename']].append(chunk['old_lineno'])
147
147
148 commit_authors = []
148 commit_authors = []
149 user_counts = {}
149 user_counts = {}
150 email_counts = {}
150 email_counts = {}
151 author_counts = {}
151 author_counts = {}
152 _commit_cache = {}
152 _commit_cache = {}
153
153
154 commits = []
154 commits = []
155 if get_commit_authors:
155 if get_commit_authors:
156 log.debug('Obtaining commit authors from set of commits')
156 log.debug('Obtaining commit authors from set of commits')
157 _compare_data = target_scm.compare(
157 _compare_data = target_scm.compare(
158 target_ref, source_ref, source_scm, merge=True,
158 target_ref, source_ref, source_scm, merge=True,
159 pre_load=["author", "date", "message"]
159 pre_load=["author", "date", "message"]
160 )
160 )
161
161
162 for commit in _compare_data:
162 for commit in _compare_data:
163 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
163 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
164 # at this function which is later called via JSON serialization
164 # at this function which is later called via JSON serialization
165 serialized_commit = dict(
165 serialized_commit = dict(
166 author=commit.author,
166 author=commit.author,
167 date=commit.date,
167 date=commit.date,
168 message=commit.message,
168 message=commit.message,
169 commit_id=commit.raw_id,
169 commit_id=commit.raw_id,
170 raw_id=commit.raw_id
170 raw_id=commit.raw_id
171 )
171 )
172 commits.append(serialized_commit)
172 commits.append(serialized_commit)
173 user = User.get_from_cs_author(serialized_commit['author'])
173 user = User.get_from_cs_author(serialized_commit['author'])
174 if user and user not in commit_authors:
174 if user and user not in commit_authors:
175 commit_authors.append(user)
175 commit_authors.append(user)
176
176
177 # lines
177 # lines
178 if get_authors:
178 if get_authors:
179 log.debug('Calculating authors of changed files')
179 log.debug('Calculating authors of changed files')
180 target_commit = source_repo.get_commit(ancestor_id)
180 target_commit = source_repo.get_commit(ancestor_id)
181
181
182 for fname, lines in changed_lines.items():
182 for fname, lines in changed_lines.items():
183
183
184 try:
184 try:
185 node = target_commit.get_node(fname, pre_load=["is_binary"])
185 node = target_commit.get_node(fname, pre_load=["is_binary"])
186 except Exception:
186 except Exception:
187 log.exception("Failed to load node with path %s", fname)
187 log.exception("Failed to load node with path %s", fname)
188 continue
188 continue
189
189
190 if not isinstance(node, FileNode):
190 if not isinstance(node, FileNode):
191 continue
191 continue
192
192
193 # NOTE(marcink): for binary node we don't do annotation, just use last author
193 # NOTE(marcink): for binary node we don't do annotation, just use last author
194 if node.is_binary:
194 if node.is_binary:
195 author = node.last_commit.author
195 author = node.last_commit.author
196 email = node.last_commit.author_email
196 email = node.last_commit.author_email
197
197
198 user = User.get_from_cs_author(author)
198 user = User.get_from_cs_author(author)
199 if user:
199 if user:
200 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
200 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
201 author_counts[author] = author_counts.get(author, 0) + 1
201 author_counts[author] = author_counts.get(author, 0) + 1
202 email_counts[email] = email_counts.get(email, 0) + 1
202 email_counts[email] = email_counts.get(email, 0) + 1
203
203
204 continue
204 continue
205
205
206 for annotation in node.annotate:
206 for annotation in node.annotate:
207 line_no, commit_id, get_commit_func, line_text = annotation
207 line_no, commit_id, get_commit_func, line_text = annotation
208 if line_no in lines:
208 if line_no in lines:
209 if commit_id not in _commit_cache:
209 if commit_id not in _commit_cache:
210 _commit_cache[commit_id] = get_commit_func()
210 _commit_cache[commit_id] = get_commit_func()
211 commit = _commit_cache[commit_id]
211 commit = _commit_cache[commit_id]
212 author = commit.author
212 author = commit.author
213 email = commit.author_email
213 email = commit.author_email
214 user = User.get_from_cs_author(author)
214 user = User.get_from_cs_author(author)
215 if user:
215 if user:
216 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
216 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
217 author_counts[author] = author_counts.get(author, 0) + 1
217 author_counts[author] = author_counts.get(author, 0) + 1
218 email_counts[email] = email_counts.get(email, 0) + 1
218 email_counts[email] = email_counts.get(email, 0) + 1
219
219
220 log.debug('Default reviewers processing finished')
220 log.debug('Default reviewers processing finished')
221
221
222 return {
222 return {
223 'commits': commits,
223 'commits': commits,
224 'files': all_files_changes,
224 'files': all_files_changes,
225 'stats': stats,
225 'stats': stats,
226 'ancestor': ancestor_id,
226 'ancestor': ancestor_id,
227 # original authors of modified files
227 # original authors of modified files
228 'original_authors': {
228 'original_authors': {
229 'users': user_counts,
229 'users': user_counts,
230 'authors': author_counts,
230 'authors': author_counts,
231 'emails': email_counts,
231 'emails': email_counts,
232 },
232 },
233 'commit_authors': commit_authors
233 'commit_authors': commit_authors
234 }
234 }
235
235
236
236
237 class PullRequestModel(BaseModel):
237 class PullRequestModel(BaseModel):
238
238
239 cls = PullRequest
239 cls = PullRequest
240
240
241 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
241 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
242
242
243 UPDATE_STATUS_MESSAGES = {
243 UPDATE_STATUS_MESSAGES = {
244 UpdateFailureReason.NONE: lazy_ugettext(
244 UpdateFailureReason.NONE: lazy_ugettext(
245 'Pull request update successful.'),
245 'Pull request update successful.'),
246 UpdateFailureReason.UNKNOWN: lazy_ugettext(
246 UpdateFailureReason.UNKNOWN: lazy_ugettext(
247 'Pull request update failed because of an unknown error.'),
247 'Pull request update failed because of an unknown error.'),
248 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
248 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
249 'No update needed because the source and target have not changed.'),
249 'No update needed because the source and target have not changed.'),
250 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
250 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
251 'Pull request cannot be updated because the reference type is '
251 'Pull request cannot be updated because the reference type is '
252 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
252 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
253 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
253 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
254 'This pull request cannot be updated because the target '
254 'This pull request cannot be updated because the target '
255 'reference is missing.'),
255 'reference is missing.'),
256 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
256 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
257 'This pull request cannot be updated because the source '
257 'This pull request cannot be updated because the source '
258 'reference is missing.'),
258 'reference is missing.'),
259 }
259 }
260 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
260 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
261 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
261 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
262
262
263 def __get_pull_request(self, pull_request):
263 def __get_pull_request(self, pull_request):
264 return self._get_instance((
264 return self._get_instance((
265 PullRequest, PullRequestVersion), pull_request)
265 PullRequest, PullRequestVersion), pull_request)
266
266
267 def _check_perms(self, perms, pull_request, user, api=False):
267 def _check_perms(self, perms, pull_request, user, api=False):
268 if not api:
268 if not api:
269 return h.HasRepoPermissionAny(*perms)(
269 return h.HasRepoPermissionAny(*perms)(
270 user=user, repo_name=pull_request.target_repo.repo_name)
270 user=user, repo_name=pull_request.target_repo.repo_name)
271 else:
271 else:
272 return h.HasRepoPermissionAnyApi(*perms)(
272 return h.HasRepoPermissionAnyApi(*perms)(
273 user=user, repo_name=pull_request.target_repo.repo_name)
273 user=user, repo_name=pull_request.target_repo.repo_name)
274
274
275 def check_user_read(self, pull_request, user, api=False):
275 def check_user_read(self, pull_request, user, api=False):
276 _perms = ('repository.admin', 'repository.write', 'repository.read',)
276 _perms = ('repository.admin', 'repository.write', 'repository.read',)
277 return self._check_perms(_perms, pull_request, user, api)
277 return self._check_perms(_perms, pull_request, user, api)
278
278
279 def check_user_merge(self, pull_request, user, api=False):
279 def check_user_merge(self, pull_request, user, api=False):
280 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
280 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
281 return self._check_perms(_perms, pull_request, user, api)
281 return self._check_perms(_perms, pull_request, user, api)
282
282
283 def check_user_update(self, pull_request, user, api=False):
283 def check_user_update(self, pull_request, user, api=False):
284 owner = user.user_id == pull_request.user_id
284 owner = user.user_id == pull_request.user_id
285 return self.check_user_merge(pull_request, user, api) or owner
285 return self.check_user_merge(pull_request, user, api) or owner
286
286
287 def check_user_delete(self, pull_request, user):
287 def check_user_delete(self, pull_request, user):
288 owner = user.user_id == pull_request.user_id
288 owner = user.user_id == pull_request.user_id
289 _perms = ('repository.admin',)
289 _perms = ('repository.admin',)
290 return self._check_perms(_perms, pull_request, user) or owner
290 return self._check_perms(_perms, pull_request, user) or owner
291
291
292 def is_user_reviewer(self, pull_request, user):
292 def is_user_reviewer(self, pull_request, user):
293 return user.user_id in [
293 return user.user_id in [
294 x.user_id for x in
294 x.user_id for x in
295 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
295 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
296 if x.user
296 if x.user
297 ]
297 ]
298
298
299 def check_user_change_status(self, pull_request, user, api=False):
299 def check_user_change_status(self, pull_request, user, api=False):
300 return self.check_user_update(pull_request, user, api) \
300 return self.check_user_update(pull_request, user, api) \
301 or self.is_user_reviewer(pull_request, user)
301 or self.is_user_reviewer(pull_request, user)
302
302
303 def check_user_comment(self, pull_request, user):
303 def check_user_comment(self, pull_request, user):
304 owner = user.user_id == pull_request.user_id
304 owner = user.user_id == pull_request.user_id
305 return self.check_user_read(pull_request, user) or owner
305 return self.check_user_read(pull_request, user) or owner
306
306
307 def get(self, pull_request):
307 def get(self, pull_request):
308 return self.__get_pull_request(pull_request)
308 return self.__get_pull_request(pull_request)
309
309
310 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
310 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
311 statuses=None, opened_by=None, order_by=None,
311 statuses=None, opened_by=None, order_by=None,
312 order_dir='desc', only_created=False):
312 order_dir='desc', only_created=False):
313 repo = None
313 repo = None
314 if repo_name:
314 if repo_name:
315 repo = self._get_repo(repo_name)
315 repo = self._get_repo(repo_name)
316
316
317 q = PullRequest.query()
317 q = PullRequest.query()
318
318
319 if search_q:
319 if search_q:
320 like_expression = u'%{}%'.format(safe_unicode(search_q))
320 like_expression = u'%{}%'.format(safe_unicode(search_q))
321 q = q.join(User, User.user_id == PullRequest.user_id)
321 q = q.join(User, User.user_id == PullRequest.user_id)
322 q = q.filter(or_(
322 q = q.filter(or_(
323 cast(PullRequest.pull_request_id, String).ilike(like_expression),
323 cast(PullRequest.pull_request_id, String).ilike(like_expression),
324 User.username.ilike(like_expression),
324 User.username.ilike(like_expression),
325 PullRequest.title.ilike(like_expression),
325 PullRequest.title.ilike(like_expression),
326 PullRequest.description.ilike(like_expression),
326 PullRequest.description.ilike(like_expression),
327 ))
327 ))
328
328
329 # source or target
329 # source or target
330 if repo and source:
330 if repo and source:
331 q = q.filter(PullRequest.source_repo == repo)
331 q = q.filter(PullRequest.source_repo == repo)
332 elif repo:
332 elif repo:
333 q = q.filter(PullRequest.target_repo == repo)
333 q = q.filter(PullRequest.target_repo == repo)
334
334
335 # closed,opened
335 # closed,opened
336 if statuses:
336 if statuses:
337 q = q.filter(PullRequest.status.in_(statuses))
337 q = q.filter(PullRequest.status.in_(statuses))
338
338
339 # opened by filter
339 # opened by filter
340 if opened_by:
340 if opened_by:
341 q = q.filter(PullRequest.user_id.in_(opened_by))
341 q = q.filter(PullRequest.user_id.in_(opened_by))
342
342
343 # only get those that are in "created" state
343 # only get those that are in "created" state
344 if only_created:
344 if only_created:
345 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
345 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
346
346
347 order_map = {
347 order_map = {
348 'name_raw': PullRequest.pull_request_id,
348 'name_raw': PullRequest.pull_request_id,
349 'id': PullRequest.pull_request_id,
349 'id': PullRequest.pull_request_id,
350 'title': PullRequest.title,
350 'title': PullRequest.title,
351 'updated_on_raw': PullRequest.updated_on,
351 'updated_on_raw': PullRequest.updated_on,
352 'target_repo': PullRequest.target_repo_id
352 'target_repo': PullRequest.target_repo_id
353 }
353 }
354 if order_by and order_by in order_map:
354 if order_by and order_by in order_map:
355 if order_dir == 'asc':
355 if order_dir == 'asc':
356 q = q.order_by(order_map[order_by].asc())
356 q = q.order_by(order_map[order_by].asc())
357 else:
357 else:
358 q = q.order_by(order_map[order_by].desc())
358 q = q.order_by(order_map[order_by].desc())
359
359
360 return q
360 return q
361
361
362 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
362 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
363 opened_by=None):
363 opened_by=None):
364 """
364 """
365 Count the number of pull requests for a specific repository.
365 Count the number of pull requests for a specific repository.
366
366
367 :param repo_name: target or source repo
367 :param repo_name: target or source repo
368 :param search_q: filter by text
368 :param search_q: filter by text
369 :param source: boolean flag to specify if repo_name refers to source
369 :param source: boolean flag to specify if repo_name refers to source
370 :param statuses: list of pull request statuses
370 :param statuses: list of pull request statuses
371 :param opened_by: author user of the pull request
371 :param opened_by: author user of the pull request
372 :returns: int number of pull requests
372 :returns: int number of pull requests
373 """
373 """
374 q = self._prepare_get_all_query(
374 q = self._prepare_get_all_query(
375 repo_name, search_q=search_q, source=source, statuses=statuses,
375 repo_name, search_q=search_q, source=source, statuses=statuses,
376 opened_by=opened_by)
376 opened_by=opened_by)
377
377
378 return q.count()
378 return q.count()
379
379
380 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
380 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
381 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
381 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
382 """
382 """
383 Get all pull requests for a specific repository.
383 Get all pull requests for a specific repository.
384
384
385 :param repo_name: target or source repo
385 :param repo_name: target or source repo
386 :param search_q: filter by text
386 :param search_q: filter by text
387 :param source: boolean flag to specify if repo_name refers to source
387 :param source: boolean flag to specify if repo_name refers to source
388 :param statuses: list of pull request statuses
388 :param statuses: list of pull request statuses
389 :param opened_by: author user of the pull request
389 :param opened_by: author user of the pull request
390 :param offset: pagination offset
390 :param offset: pagination offset
391 :param length: length of returned list
391 :param length: length of returned list
392 :param order_by: order of the returned list
392 :param order_by: order of the returned list
393 :param order_dir: 'asc' or 'desc' ordering direction
393 :param order_dir: 'asc' or 'desc' ordering direction
394 :returns: list of pull requests
394 :returns: list of pull requests
395 """
395 """
396 q = self._prepare_get_all_query(
396 q = self._prepare_get_all_query(
397 repo_name, search_q=search_q, source=source, statuses=statuses,
397 repo_name, search_q=search_q, source=source, statuses=statuses,
398 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
398 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
399
399
400 if length:
400 if length:
401 pull_requests = q.limit(length).offset(offset).all()
401 pull_requests = q.limit(length).offset(offset).all()
402 else:
402 else:
403 pull_requests = q.all()
403 pull_requests = q.all()
404
404
405 return pull_requests
405 return pull_requests
406
406
407 def count_awaiting_review(self, repo_name, search_q=None, statuses=None):
407 def count_awaiting_review(self, repo_name, search_q=None, statuses=None):
408 """
408 """
409 Count the number of pull requests for a specific repository that are
409 Count the number of pull requests for a specific repository that are
410 awaiting review.
410 awaiting review.
411
411
412 :param repo_name: target or source repo
412 :param repo_name: target or source repo
413 :param search_q: filter by text
413 :param search_q: filter by text
414 :param statuses: list of pull request statuses
414 :param statuses: list of pull request statuses
415 :returns: int number of pull requests
415 :returns: int number of pull requests
416 """
416 """
417 pull_requests = self.get_awaiting_review(
417 pull_requests = self.get_awaiting_review(
418 repo_name, search_q=search_q, statuses=statuses)
418 repo_name, search_q=search_q, statuses=statuses)
419
419
420 return len(pull_requests)
420 return len(pull_requests)
421
421
422 def get_awaiting_review(self, repo_name, search_q=None, statuses=None,
422 def get_awaiting_review(self, repo_name, search_q=None, statuses=None,
423 offset=0, length=None, order_by=None, order_dir='desc'):
423 offset=0, length=None, order_by=None, order_dir='desc'):
424 """
424 """
425 Get all pull requests for a specific repository that are awaiting
425 Get all pull requests for a specific repository that are awaiting
426 review.
426 review.
427
427
428 :param repo_name: target or source repo
428 :param repo_name: target or source repo
429 :param search_q: filter by text
429 :param search_q: filter by text
430 :param statuses: list of pull request statuses
430 :param statuses: list of pull request statuses
431 :param offset: pagination offset
431 :param offset: pagination offset
432 :param length: length of returned list
432 :param length: length of returned list
433 :param order_by: order of the returned list
433 :param order_by: order of the returned list
434 :param order_dir: 'asc' or 'desc' ordering direction
434 :param order_dir: 'asc' or 'desc' ordering direction
435 :returns: list of pull requests
435 :returns: list of pull requests
436 """
436 """
437 pull_requests = self.get_all(
437 pull_requests = self.get_all(
438 repo_name, search_q=search_q, statuses=statuses,
438 repo_name, search_q=search_q, statuses=statuses,
439 order_by=order_by, order_dir=order_dir)
439 order_by=order_by, order_dir=order_dir)
440
440
441 _filtered_pull_requests = []
441 _filtered_pull_requests = []
442 for pr in pull_requests:
442 for pr in pull_requests:
443 status = pr.calculated_review_status()
443 status = pr.calculated_review_status()
444 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
444 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
445 ChangesetStatus.STATUS_UNDER_REVIEW]:
445 ChangesetStatus.STATUS_UNDER_REVIEW]:
446 _filtered_pull_requests.append(pr)
446 _filtered_pull_requests.append(pr)
447 if length:
447 if length:
448 return _filtered_pull_requests[offset:offset+length]
448 return _filtered_pull_requests[offset:offset+length]
449 else:
449 else:
450 return _filtered_pull_requests
450 return _filtered_pull_requests
451
451
452 def _prepare_awaiting_my_review_review_query(
452 def _prepare_awaiting_my_review_review_query(
453 self, repo_name, user_id, search_q=None, statuses=None,
453 self, repo_name, user_id, search_q=None, statuses=None,
454 order_by=None, order_dir='desc'):
454 order_by=None, order_dir='desc'):
455
455
456 for_review_statuses = [
456 for_review_statuses = [
457 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
457 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
458 ]
458 ]
459
459
460 pull_request_alias = aliased(PullRequest)
460 pull_request_alias = aliased(PullRequest)
461 status_alias = aliased(ChangesetStatus)
461 status_alias = aliased(ChangesetStatus)
462 reviewers_alias = aliased(PullRequestReviewers)
462 reviewers_alias = aliased(PullRequestReviewers)
463 repo_alias = aliased(Repository)
463 repo_alias = aliased(Repository)
464
464
465 last_ver_subq = Session()\
465 last_ver_subq = Session()\
466 .query(func.min(ChangesetStatus.version)) \
466 .query(func.min(ChangesetStatus.version)) \
467 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
467 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
468 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
468 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
469 .subquery()
469 .subquery()
470
470
471 q = Session().query(pull_request_alias) \
471 q = Session().query(pull_request_alias) \
472 .options(lazyload(pull_request_alias.author)) \
472 .options(lazyload(pull_request_alias.author)) \
473 .join(reviewers_alias,
473 .join(reviewers_alias,
474 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
474 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
475 .join(repo_alias,
475 .join(repo_alias,
476 repo_alias.repo_id == pull_request_alias.target_repo_id) \
476 repo_alias.repo_id == pull_request_alias.target_repo_id) \
477 .outerjoin(status_alias,
477 .outerjoin(status_alias,
478 and_(status_alias.user_id == reviewers_alias.user_id,
478 and_(status_alias.user_id == reviewers_alias.user_id,
479 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
479 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
480 .filter(or_(status_alias.version == null(),
480 .filter(or_(status_alias.version == null(),
481 status_alias.version == last_ver_subq)) \
481 status_alias.version == last_ver_subq)) \
482 .filter(reviewers_alias.user_id == user_id) \
482 .filter(reviewers_alias.user_id == user_id) \
483 .filter(repo_alias.repo_name == repo_name) \
483 .filter(repo_alias.repo_name == repo_name) \
484 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
484 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
485 .group_by(pull_request_alias)
485 .group_by(pull_request_alias)
486
486
487 # closed,opened
487 # closed,opened
488 if statuses:
488 if statuses:
489 q = q.filter(pull_request_alias.status.in_(statuses))
489 q = q.filter(pull_request_alias.status.in_(statuses))
490
490
491 if search_q:
491 if search_q:
492 like_expression = u'%{}%'.format(safe_unicode(search_q))
492 like_expression = u'%{}%'.format(safe_unicode(search_q))
493 q = q.join(User, User.user_id == pull_request_alias.user_id)
493 q = q.join(User, User.user_id == pull_request_alias.user_id)
494 q = q.filter(or_(
494 q = q.filter(or_(
495 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
495 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
496 User.username.ilike(like_expression),
496 User.username.ilike(like_expression),
497 pull_request_alias.title.ilike(like_expression),
497 pull_request_alias.title.ilike(like_expression),
498 pull_request_alias.description.ilike(like_expression),
498 pull_request_alias.description.ilike(like_expression),
499 ))
499 ))
500
500
501 order_map = {
501 order_map = {
502 'name_raw': pull_request_alias.pull_request_id,
502 'name_raw': pull_request_alias.pull_request_id,
503 'title': pull_request_alias.title,
503 'title': pull_request_alias.title,
504 'updated_on_raw': pull_request_alias.updated_on,
504 'updated_on_raw': pull_request_alias.updated_on,
505 'target_repo': pull_request_alias.target_repo_id
505 'target_repo': pull_request_alias.target_repo_id
506 }
506 }
507 if order_by and order_by in order_map:
507 if order_by and order_by in order_map:
508 if order_dir == 'asc':
508 if order_dir == 'asc':
509 q = q.order_by(order_map[order_by].asc())
509 q = q.order_by(order_map[order_by].asc())
510 else:
510 else:
511 q = q.order_by(order_map[order_by].desc())
511 q = q.order_by(order_map[order_by].desc())
512
512
513 return q
513 return q
514
514
515 def count_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None):
515 def count_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None):
516 """
516 """
517 Count the number of pull requests for a specific repository that are
517 Count the number of pull requests for a specific repository that are
518 awaiting review from a specific user.
518 awaiting review from a specific user.
519
519
520 :param repo_name: target or source repo
520 :param repo_name: target or source repo
521 :param user_id: reviewer user of the pull request
521 :param user_id: reviewer user of the pull request
522 :param search_q: filter by text
522 :param search_q: filter by text
523 :param statuses: list of pull request statuses
523 :param statuses: list of pull request statuses
524 :returns: int number of pull requests
524 :returns: int number of pull requests
525 """
525 """
526 q = self._prepare_awaiting_my_review_review_query(
526 q = self._prepare_awaiting_my_review_review_query(
527 repo_name, user_id, search_q=search_q, statuses=statuses)
527 repo_name, user_id, search_q=search_q, statuses=statuses)
528 return q.count()
528 return q.count()
529
529
530 def get_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None,
530 def get_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None,
531 offset=0, length=None, order_by=None, order_dir='desc'):
531 offset=0, length=None, order_by=None, order_dir='desc'):
532 """
532 """
533 Get all pull requests for a specific repository that are awaiting
533 Get all pull requests for a specific repository that are awaiting
534 review from a specific user.
534 review from a specific user.
535
535
536 :param repo_name: target or source repo
536 :param repo_name: target or source repo
537 :param user_id: reviewer user of the pull request
537 :param user_id: reviewer user of the pull request
538 :param search_q: filter by text
538 :param search_q: filter by text
539 :param statuses: list of pull request statuses
539 :param statuses: list of pull request statuses
540 :param offset: pagination offset
540 :param offset: pagination offset
541 :param length: length of returned list
541 :param length: length of returned list
542 :param order_by: order of the returned list
542 :param order_by: order of the returned list
543 :param order_dir: 'asc' or 'desc' ordering direction
543 :param order_dir: 'asc' or 'desc' ordering direction
544 :returns: list of pull requests
544 :returns: list of pull requests
545 """
545 """
546
546
547 q = self._prepare_awaiting_my_review_review_query(
547 q = self._prepare_awaiting_my_review_review_query(
548 repo_name, user_id, search_q=search_q, statuses=statuses,
548 repo_name, user_id, search_q=search_q, statuses=statuses,
549 order_by=order_by, order_dir=order_dir)
549 order_by=order_by, order_dir=order_dir)
550
550
551 if length:
551 if length:
552 pull_requests = q.limit(length).offset(offset).all()
552 pull_requests = q.limit(length).offset(offset).all()
553 else:
553 else:
554 pull_requests = q.all()
554 pull_requests = q.all()
555
555
556 return pull_requests
556 return pull_requests
557
557
558 def _prepare_im_participating_query(self, user_id=None, statuses=None, query='',
558 def _prepare_im_participating_query(self, user_id=None, statuses=None, query='',
559 order_by=None, order_dir='desc'):
559 order_by=None, order_dir='desc'):
560 """
560 """
561 return a query of pull-requests user is an creator, or he's added as a reviewer
561 return a query of pull-requests user is an creator, or he's added as a reviewer
562 """
562 """
563 q = PullRequest.query()
563 q = PullRequest.query()
564 if user_id:
564 if user_id:
565 reviewers_subquery = Session().query(
565 reviewers_subquery = Session().query(
566 PullRequestReviewers.pull_request_id).filter(
566 PullRequestReviewers.pull_request_id).filter(
567 PullRequestReviewers.user_id == user_id).subquery()
567 PullRequestReviewers.user_id == user_id).subquery()
568 user_filter = or_(
568 user_filter = or_(
569 PullRequest.user_id == user_id,
569 PullRequest.user_id == user_id,
570 PullRequest.pull_request_id.in_(reviewers_subquery)
570 PullRequest.pull_request_id.in_(reviewers_subquery)
571 )
571 )
572 q = PullRequest.query().filter(user_filter)
572 q = PullRequest.query().filter(user_filter)
573
573
574 # closed,opened
574 # closed,opened
575 if statuses:
575 if statuses:
576 q = q.filter(PullRequest.status.in_(statuses))
576 q = q.filter(PullRequest.status.in_(statuses))
577
577
578 if query:
578 if query:
579 like_expression = u'%{}%'.format(safe_unicode(query))
579 like_expression = u'%{}%'.format(safe_unicode(query))
580 q = q.join(User, User.user_id == PullRequest.user_id)
580 q = q.join(User, User.user_id == PullRequest.user_id)
581 q = q.filter(or_(
581 q = q.filter(or_(
582 cast(PullRequest.pull_request_id, String).ilike(like_expression),
582 cast(PullRequest.pull_request_id, String).ilike(like_expression),
583 User.username.ilike(like_expression),
583 User.username.ilike(like_expression),
584 PullRequest.title.ilike(like_expression),
584 PullRequest.title.ilike(like_expression),
585 PullRequest.description.ilike(like_expression),
585 PullRequest.description.ilike(like_expression),
586 ))
586 ))
587
587
588 order_map = {
588 order_map = {
589 'name_raw': PullRequest.pull_request_id,
589 'name_raw': PullRequest.pull_request_id,
590 'title': PullRequest.title,
590 'title': PullRequest.title,
591 'updated_on_raw': PullRequest.updated_on,
591 'updated_on_raw': PullRequest.updated_on,
592 'target_repo': PullRequest.target_repo_id
592 'target_repo': PullRequest.target_repo_id
593 }
593 }
594 if order_by and order_by in order_map:
594 if order_by and order_by in order_map:
595 if order_dir == 'asc':
595 if order_dir == 'asc':
596 q = q.order_by(order_map[order_by].asc())
596 q = q.order_by(order_map[order_by].asc())
597 else:
597 else:
598 q = q.order_by(order_map[order_by].desc())
598 q = q.order_by(order_map[order_by].desc())
599
599
600 return q
600 return q
601
601
602 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
602 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
603 q = self._prepare_im_participating_query(user_id, statuses=statuses, query=query)
603 q = self._prepare_im_participating_query(user_id, statuses=statuses, query=query)
604 return q.count()
604 return q.count()
605
605
606 def get_im_participating_in(
606 def get_im_participating_in(
607 self, user_id=None, statuses=None, query='', offset=0,
607 self, user_id=None, statuses=None, query='', offset=0,
608 length=None, order_by=None, order_dir='desc'):
608 length=None, order_by=None, order_dir='desc'):
609 """
609 """
610 Get all Pull requests that i'm participating in as a reviewer, or i have opened
610 Get all Pull requests that i'm participating in as a reviewer, or i have opened
611 """
611 """
612
612
613 q = self._prepare_im_participating_query(
613 q = self._prepare_im_participating_query(
614 user_id, statuses=statuses, query=query, order_by=order_by,
614 user_id, statuses=statuses, query=query, order_by=order_by,
615 order_dir=order_dir)
615 order_dir=order_dir)
616
616
617 if length:
617 if length:
618 pull_requests = q.limit(length).offset(offset).all()
618 pull_requests = q.limit(length).offset(offset).all()
619 else:
619 else:
620 pull_requests = q.all()
620 pull_requests = q.all()
621
621
622 return pull_requests
622 return pull_requests
623
623
624 def _prepare_participating_in_for_review_query(
624 def _prepare_participating_in_for_review_query(
625 self, user_id, statuses=None, query='', order_by=None, order_dir='desc'):
625 self, user_id, statuses=None, query='', order_by=None, order_dir='desc'):
626
626
627 for_review_statuses = [
627 for_review_statuses = [
628 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
628 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
629 ]
629 ]
630
630
631 pull_request_alias = aliased(PullRequest)
631 pull_request_alias = aliased(PullRequest)
632 status_alias = aliased(ChangesetStatus)
632 status_alias = aliased(ChangesetStatus)
633 reviewers_alias = aliased(PullRequestReviewers)
633 reviewers_alias = aliased(PullRequestReviewers)
634
634
635 last_ver_subq = Session()\
635 last_ver_subq = Session()\
636 .query(func.min(ChangesetStatus.version)) \
636 .query(func.min(ChangesetStatus.version)) \
637 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
637 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
638 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
638 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
639 .subquery()
639 .subquery()
640
640
641 q = Session().query(pull_request_alias) \
641 q = Session().query(pull_request_alias) \
642 .options(lazyload(pull_request_alias.author)) \
642 .options(lazyload(pull_request_alias.author)) \
643 .join(reviewers_alias,
643 .join(reviewers_alias,
644 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
644 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
645 .outerjoin(status_alias,
645 .outerjoin(status_alias,
646 and_(status_alias.user_id == reviewers_alias.user_id,
646 and_(status_alias.user_id == reviewers_alias.user_id,
647 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
647 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
648 .filter(or_(status_alias.version == null(),
648 .filter(or_(status_alias.version == null(),
649 status_alias.version == last_ver_subq)) \
649 status_alias.version == last_ver_subq)) \
650 .filter(reviewers_alias.user_id == user_id) \
650 .filter(reviewers_alias.user_id == user_id) \
651 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
651 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
652 .group_by(pull_request_alias)
652 .group_by(pull_request_alias)
653
653
654 # closed,opened
654 # closed,opened
655 if statuses:
655 if statuses:
656 q = q.filter(pull_request_alias.status.in_(statuses))
656 q = q.filter(pull_request_alias.status.in_(statuses))
657
657
658 if query:
658 if query:
659 like_expression = u'%{}%'.format(safe_unicode(query))
659 like_expression = u'%{}%'.format(safe_unicode(query))
660 q = q.join(User, User.user_id == pull_request_alias.user_id)
660 q = q.join(User, User.user_id == pull_request_alias.user_id)
661 q = q.filter(or_(
661 q = q.filter(or_(
662 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
662 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
663 User.username.ilike(like_expression),
663 User.username.ilike(like_expression),
664 pull_request_alias.title.ilike(like_expression),
664 pull_request_alias.title.ilike(like_expression),
665 pull_request_alias.description.ilike(like_expression),
665 pull_request_alias.description.ilike(like_expression),
666 ))
666 ))
667
667
668 order_map = {
668 order_map = {
669 'name_raw': pull_request_alias.pull_request_id,
669 'name_raw': pull_request_alias.pull_request_id,
670 'title': pull_request_alias.title,
670 'title': pull_request_alias.title,
671 'updated_on_raw': pull_request_alias.updated_on,
671 'updated_on_raw': pull_request_alias.updated_on,
672 'target_repo': pull_request_alias.target_repo_id
672 'target_repo': pull_request_alias.target_repo_id
673 }
673 }
674 if order_by and order_by in order_map:
674 if order_by and order_by in order_map:
675 if order_dir == 'asc':
675 if order_dir == 'asc':
676 q = q.order_by(order_map[order_by].asc())
676 q = q.order_by(order_map[order_by].asc())
677 else:
677 else:
678 q = q.order_by(order_map[order_by].desc())
678 q = q.order_by(order_map[order_by].desc())
679
679
680 return q
680 return q
681
681
682 def count_im_participating_in_for_review(self, user_id, statuses=None, query=''):
682 def count_im_participating_in_for_review(self, user_id, statuses=None, query=''):
683 q = self._prepare_participating_in_for_review_query(user_id, statuses=statuses, query=query)
683 q = self._prepare_participating_in_for_review_query(user_id, statuses=statuses, query=query)
684 return q.count()
684 return q.count()
685
685
686 def get_im_participating_in_for_review(
686 def get_im_participating_in_for_review(
687 self, user_id, statuses=None, query='', offset=0,
687 self, user_id, statuses=None, query='', offset=0,
688 length=None, order_by=None, order_dir='desc'):
688 length=None, order_by=None, order_dir='desc'):
689 """
689 """
690 Get all Pull requests that needs user approval or rejection
690 Get all Pull requests that needs user approval or rejection
691 """
691 """
692
692
693 q = self._prepare_participating_in_for_review_query(
693 q = self._prepare_participating_in_for_review_query(
694 user_id, statuses=statuses, query=query, order_by=order_by,
694 user_id, statuses=statuses, query=query, order_by=order_by,
695 order_dir=order_dir)
695 order_dir=order_dir)
696
696
697 if length:
697 if length:
698 pull_requests = q.limit(length).offset(offset).all()
698 pull_requests = q.limit(length).offset(offset).all()
699 else:
699 else:
700 pull_requests = q.all()
700 pull_requests = q.all()
701
701
702 return pull_requests
702 return pull_requests
703
703
704 def get_versions(self, pull_request):
704 def get_versions(self, pull_request):
705 """
705 """
706 returns version of pull request sorted by ID descending
706 returns version of pull request sorted by ID descending
707 """
707 """
708 return PullRequestVersion.query()\
708 return PullRequestVersion.query()\
709 .filter(PullRequestVersion.pull_request == pull_request)\
709 .filter(PullRequestVersion.pull_request == pull_request)\
710 .order_by(PullRequestVersion.pull_request_version_id.asc())\
710 .order_by(PullRequestVersion.pull_request_version_id.asc())\
711 .all()
711 .all()
712
712
713 def get_pr_version(self, pull_request_id, version=None):
713 def get_pr_version(self, pull_request_id, version=None):
714 at_version = None
714 at_version = None
715
715
716 if version and version == 'latest':
716 if version and version == 'latest':
717 pull_request_ver = PullRequest.get(pull_request_id)
717 pull_request_ver = PullRequest.get(pull_request_id)
718 pull_request_obj = pull_request_ver
718 pull_request_obj = pull_request_ver
719 _org_pull_request_obj = pull_request_obj
719 _org_pull_request_obj = pull_request_obj
720 at_version = 'latest'
720 at_version = 'latest'
721 elif version:
721 elif version:
722 pull_request_ver = PullRequestVersion.get_or_404(version)
722 pull_request_ver = PullRequestVersion.get_or_404(version)
723 pull_request_obj = pull_request_ver
723 pull_request_obj = pull_request_ver
724 _org_pull_request_obj = pull_request_ver.pull_request
724 _org_pull_request_obj = pull_request_ver.pull_request
725 at_version = pull_request_ver.pull_request_version_id
725 at_version = pull_request_ver.pull_request_version_id
726 else:
726 else:
727 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
727 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
728 pull_request_id)
728 pull_request_id)
729
729
730 pull_request_display_obj = PullRequest.get_pr_display_object(
730 pull_request_display_obj = PullRequest.get_pr_display_object(
731 pull_request_obj, _org_pull_request_obj)
731 pull_request_obj, _org_pull_request_obj)
732
732
733 return _org_pull_request_obj, pull_request_obj, \
733 return _org_pull_request_obj, pull_request_obj, \
734 pull_request_display_obj, at_version
734 pull_request_display_obj, at_version
735
735
736 def pr_commits_versions(self, versions):
736 def pr_commits_versions(self, versions):
737 """
737 """
738 Maps the pull-request commits into all known PR versions. This way we can obtain
738 Maps the pull-request commits into all known PR versions. This way we can obtain
739 each pr version the commit was introduced in.
739 each pr version the commit was introduced in.
740 """
740 """
741 commit_versions = collections.defaultdict(list)
741 commit_versions = collections.defaultdict(list)
742 num_versions = [x.pull_request_version_id for x in versions]
742 num_versions = [x.pull_request_version_id for x in versions]
743 for ver in versions:
743 for ver in versions:
744 for commit_id in ver.revisions:
744 for commit_id in ver.revisions:
745 ver_idx = ChangesetComment.get_index_from_version(
745 ver_idx = ChangesetComment.get_index_from_version(
746 ver.pull_request_version_id, num_versions=num_versions)
746 ver.pull_request_version_id, num_versions=num_versions)
747 commit_versions[commit_id].append(ver_idx)
747 commit_versions[commit_id].append(ver_idx)
748 return commit_versions
748 return commit_versions
749
749
750 def create(self, created_by, source_repo, source_ref, target_repo,
750 def create(self, created_by, source_repo, source_ref, target_repo,
751 target_ref, revisions, reviewers, observers, title, description=None,
751 target_ref, revisions, reviewers, observers, title, description=None,
752 common_ancestor_id=None,
752 common_ancestor_id=None,
753 description_renderer=None,
753 description_renderer=None,
754 reviewer_data=None, translator=None, auth_user=None):
754 reviewer_data=None, translator=None, auth_user=None):
755 translator = translator or get_current_request().translate
755 translator = translator or get_current_request().translate
756
756
757 created_by_user = self._get_user(created_by)
757 created_by_user = self._get_user(created_by)
758 auth_user = auth_user or created_by_user.AuthUser()
758 auth_user = auth_user or created_by_user.AuthUser()
759 source_repo = self._get_repo(source_repo)
759 source_repo = self._get_repo(source_repo)
760 target_repo = self._get_repo(target_repo)
760 target_repo = self._get_repo(target_repo)
761
761
762 pull_request = PullRequest()
762 pull_request = PullRequest()
763 pull_request.source_repo = source_repo
763 pull_request.source_repo = source_repo
764 pull_request.source_ref = source_ref
764 pull_request.source_ref = source_ref
765 pull_request.target_repo = target_repo
765 pull_request.target_repo = target_repo
766 pull_request.target_ref = target_ref
766 pull_request.target_ref = target_ref
767 pull_request.revisions = revisions
767 pull_request.revisions = revisions
768 pull_request.title = title
768 pull_request.title = title
769 pull_request.description = description
769 pull_request.description = description
770 pull_request.description_renderer = description_renderer
770 pull_request.description_renderer = description_renderer
771 pull_request.author = created_by_user
771 pull_request.author = created_by_user
772 pull_request.reviewer_data = reviewer_data
772 pull_request.reviewer_data = reviewer_data
773 pull_request.pull_request_state = pull_request.STATE_CREATING
773 pull_request.pull_request_state = pull_request.STATE_CREATING
774 pull_request.common_ancestor_id = common_ancestor_id
774 pull_request.common_ancestor_id = common_ancestor_id
775
775
776 Session().add(pull_request)
776 Session().add(pull_request)
777 Session().flush()
777 Session().flush()
778
778
779 reviewer_ids = set()
779 reviewer_ids = set()
780 # members / reviewers
780 # members / reviewers
781 for reviewer_object in reviewers:
781 for reviewer_object in reviewers:
782 user_id, reasons, mandatory, role, rules = reviewer_object
782 user_id, reasons, mandatory, role, rules = reviewer_object
783 user = self._get_user(user_id)
783 user = self._get_user(user_id)
784
784
785 # skip duplicates
785 # skip duplicates
786 if user.user_id in reviewer_ids:
786 if user.user_id in reviewer_ids:
787 continue
787 continue
788
788
789 reviewer_ids.add(user.user_id)
789 reviewer_ids.add(user.user_id)
790
790
791 reviewer = PullRequestReviewers()
791 reviewer = PullRequestReviewers()
792 reviewer.user = user
792 reviewer.user = user
793 reviewer.pull_request = pull_request
793 reviewer.pull_request = pull_request
794 reviewer.reasons = reasons
794 reviewer.reasons = reasons
795 reviewer.mandatory = mandatory
795 reviewer.mandatory = mandatory
796 reviewer.role = role
796 reviewer.role = role
797
797
798 # NOTE(marcink): pick only first rule for now
798 # NOTE(marcink): pick only first rule for now
799 rule_id = list(rules)[0] if rules else None
799 rule_id = list(rules)[0] if rules else None
800 rule = RepoReviewRule.get(rule_id) if rule_id else None
800 rule = RepoReviewRule.get(rule_id) if rule_id else None
801 if rule:
801 if rule:
802 review_group = rule.user_group_vote_rule(user_id)
802 review_group = rule.user_group_vote_rule(user_id)
803 # we check if this particular reviewer is member of a voting group
803 # we check if this particular reviewer is member of a voting group
804 if review_group:
804 if review_group:
805 # NOTE(marcink):
805 # NOTE(marcink):
806 # can be that user is member of more but we pick the first same,
806 # can be that user is member of more but we pick the first same,
807 # same as default reviewers algo
807 # same as default reviewers algo
808 review_group = review_group[0]
808 review_group = review_group[0]
809
809
810 rule_data = {
810 rule_data = {
811 'rule_name':
811 'rule_name':
812 rule.review_rule_name,
812 rule.review_rule_name,
813 'rule_user_group_entry_id':
813 'rule_user_group_entry_id':
814 review_group.repo_review_rule_users_group_id,
814 review_group.repo_review_rule_users_group_id,
815 'rule_user_group_name':
815 'rule_user_group_name':
816 review_group.users_group.users_group_name,
816 review_group.users_group.users_group_name,
817 'rule_user_group_members':
817 'rule_user_group_members':
818 [x.user.username for x in review_group.users_group.members],
818 [x.user.username for x in review_group.users_group.members],
819 'rule_user_group_members_id':
819 'rule_user_group_members_id':
820 [x.user.user_id for x in review_group.users_group.members],
820 [x.user.user_id for x in review_group.users_group.members],
821 }
821 }
822 # e.g {'vote_rule': -1, 'mandatory': True}
822 # e.g {'vote_rule': -1, 'mandatory': True}
823 rule_data.update(review_group.rule_data())
823 rule_data.update(review_group.rule_data())
824
824
825 reviewer.rule_data = rule_data
825 reviewer.rule_data = rule_data
826
826
827 Session().add(reviewer)
827 Session().add(reviewer)
828 Session().flush()
828 Session().flush()
829
829
830 for observer_object in observers:
830 for observer_object in observers:
831 user_id, reasons, mandatory, role, rules = observer_object
831 user_id, reasons, mandatory, role, rules = observer_object
832 user = self._get_user(user_id)
832 user = self._get_user(user_id)
833
833
834 # skip duplicates from reviewers
834 # skip duplicates from reviewers
835 if user.user_id in reviewer_ids:
835 if user.user_id in reviewer_ids:
836 continue
836 continue
837
837
838 #reviewer_ids.add(user.user_id)
838 #reviewer_ids.add(user.user_id)
839
839
840 observer = PullRequestReviewers()
840 observer = PullRequestReviewers()
841 observer.user = user
841 observer.user = user
842 observer.pull_request = pull_request
842 observer.pull_request = pull_request
843 observer.reasons = reasons
843 observer.reasons = reasons
844 observer.mandatory = mandatory
844 observer.mandatory = mandatory
845 observer.role = role
845 observer.role = role
846
846
847 # NOTE(marcink): pick only first rule for now
847 # NOTE(marcink): pick only first rule for now
848 rule_id = list(rules)[0] if rules else None
848 rule_id = list(rules)[0] if rules else None
849 rule = RepoReviewRule.get(rule_id) if rule_id else None
849 rule = RepoReviewRule.get(rule_id) if rule_id else None
850 if rule:
850 if rule:
851 # TODO(marcink): do we need this for observers ??
851 # TODO(marcink): do we need this for observers ??
852 pass
852 pass
853
853
854 Session().add(observer)
854 Session().add(observer)
855 Session().flush()
855 Session().flush()
856
856
857 # Set approval status to "Under Review" for all commits which are
857 # Set approval status to "Under Review" for all commits which are
858 # part of this pull request.
858 # part of this pull request.
859 ChangesetStatusModel().set_status(
859 ChangesetStatusModel().set_status(
860 repo=target_repo,
860 repo=target_repo,
861 status=ChangesetStatus.STATUS_UNDER_REVIEW,
861 status=ChangesetStatus.STATUS_UNDER_REVIEW,
862 user=created_by_user,
862 user=created_by_user,
863 pull_request=pull_request
863 pull_request=pull_request
864 )
864 )
865 # we commit early at this point. This has to do with a fact
865 # we commit early at this point. This has to do with a fact
866 # that before queries do some row-locking. And because of that
866 # that before queries do some row-locking. And because of that
867 # we need to commit and finish transaction before below validate call
867 # we need to commit and finish transaction before below validate call
868 # that for large repos could be long resulting in long row locks
868 # that for large repos could be long resulting in long row locks
869 Session().commit()
869 Session().commit()
870
870
871 # prepare workspace, and run initial merge simulation. Set state during that
871 # prepare workspace, and run initial merge simulation. Set state during that
872 # operation
872 # operation
873 pull_request = PullRequest.get(pull_request.pull_request_id)
873 pull_request = PullRequest.get(pull_request.pull_request_id)
874
874
875 # set as merging, for merge simulation, and if finished to created so we mark
875 # set as merging, for merge simulation, and if finished to created so we mark
876 # simulation is working fine
876 # simulation is working fine
877 with pull_request.set_state(PullRequest.STATE_MERGING,
877 with pull_request.set_state(PullRequest.STATE_MERGING,
878 final_state=PullRequest.STATE_CREATED) as state_obj:
878 final_state=PullRequest.STATE_CREATED) as state_obj:
879 MergeCheck.validate(
879 MergeCheck.validate(
880 pull_request, auth_user=auth_user, translator=translator)
880 pull_request, auth_user=auth_user, translator=translator)
881
881
882 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
882 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
883 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
883 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
884
884
885 creation_data = pull_request.get_api_data(with_merge_state=False)
885 creation_data = pull_request.get_api_data(with_merge_state=False)
886 self._log_audit_action(
886 self._log_audit_action(
887 'repo.pull_request.create', {'data': creation_data},
887 'repo.pull_request.create', {'data': creation_data},
888 auth_user, pull_request)
888 auth_user, pull_request)
889
889
890 return pull_request
890 return pull_request
891
891
892 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
892 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
893 pull_request = self.__get_pull_request(pull_request)
893 pull_request = self.__get_pull_request(pull_request)
894 target_scm = pull_request.target_repo.scm_instance()
894 target_scm = pull_request.target_repo.scm_instance()
895 if action == 'create':
895 if action == 'create':
896 trigger_hook = hooks_utils.trigger_create_pull_request_hook
896 trigger_hook = hooks_utils.trigger_create_pull_request_hook
897 elif action == 'merge':
897 elif action == 'merge':
898 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
898 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
899 elif action == 'close':
899 elif action == 'close':
900 trigger_hook = hooks_utils.trigger_close_pull_request_hook
900 trigger_hook = hooks_utils.trigger_close_pull_request_hook
901 elif action == 'review_status_change':
901 elif action == 'review_status_change':
902 trigger_hook = hooks_utils.trigger_review_pull_request_hook
902 trigger_hook = hooks_utils.trigger_review_pull_request_hook
903 elif action == 'update':
903 elif action == 'update':
904 trigger_hook = hooks_utils.trigger_update_pull_request_hook
904 trigger_hook = hooks_utils.trigger_update_pull_request_hook
905 elif action == 'comment':
905 elif action == 'comment':
906 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
906 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
907 elif action == 'comment_edit':
907 elif action == 'comment_edit':
908 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
908 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
909 else:
909 else:
910 return
910 return
911
911
912 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
912 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
913 pull_request, action, trigger_hook)
913 pull_request, action, trigger_hook)
914 trigger_hook(
914 trigger_hook(
915 username=user.username,
915 username=user.username,
916 repo_name=pull_request.target_repo.repo_name,
916 repo_name=pull_request.target_repo.repo_name,
917 repo_type=target_scm.alias,
917 repo_type=target_scm.alias,
918 pull_request=pull_request,
918 pull_request=pull_request,
919 data=data)
919 data=data)
920
920
921 def _get_commit_ids(self, pull_request):
921 def _get_commit_ids(self, pull_request):
922 """
922 """
923 Return the commit ids of the merged pull request.
923 Return the commit ids of the merged pull request.
924
924
925 This method is not dealing correctly yet with the lack of autoupdates
925 This method is not dealing correctly yet with the lack of autoupdates
926 nor with the implicit target updates.
926 nor with the implicit target updates.
927 For example: if a commit in the source repo is already in the target it
927 For example: if a commit in the source repo is already in the target it
928 will be reported anyways.
928 will be reported anyways.
929 """
929 """
930 merge_rev = pull_request.merge_rev
930 merge_rev = pull_request.merge_rev
931 if merge_rev is None:
931 if merge_rev is None:
932 raise ValueError('This pull request was not merged yet')
932 raise ValueError('This pull request was not merged yet')
933
933
934 commit_ids = list(pull_request.revisions)
934 commit_ids = list(pull_request.revisions)
935 if merge_rev not in commit_ids:
935 if merge_rev not in commit_ids:
936 commit_ids.append(merge_rev)
936 commit_ids.append(merge_rev)
937
937
938 return commit_ids
938 return commit_ids
939
939
940 def merge_repo(self, pull_request, user, extras):
940 def merge_repo(self, pull_request, user, extras):
941 repo_type = pull_request.source_repo.repo_type
941 repo_type = pull_request.source_repo.repo_type
942 log.debug("Merging pull request %s", pull_request.pull_request_id)
942 log.debug("Merging pull request %s", pull_request.pull_request_id)
943 extras['user_agent'] = '{}/internal-merge'.format(repo_type)
943 extras['user_agent'] = '{}/internal-merge'.format(repo_type)
944 merge_state = self._merge_pull_request(pull_request, user, extras)
944 merge_state = self._merge_pull_request(pull_request, user, extras)
945 if merge_state.executed:
945 if merge_state.executed:
946 log.debug("Merge was successful, updating the pull request comments.")
946 log.debug("Merge was successful, updating the pull request comments.")
947 self._comment_and_close_pr(pull_request, user, merge_state)
947 self._comment_and_close_pr(pull_request, user, merge_state)
948
948
949 self._log_audit_action(
949 self._log_audit_action(
950 'repo.pull_request.merge',
950 'repo.pull_request.merge',
951 {'merge_state': merge_state.__dict__},
951 {'merge_state': merge_state.__dict__},
952 user, pull_request)
952 user, pull_request)
953
953
954 else:
954 else:
955 log.warn("Merge failed, not updating the pull request.")
955 log.warn("Merge failed, not updating the pull request.")
956 return merge_state
956 return merge_state
957
957
958 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
958 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
959 target_vcs = pull_request.target_repo.scm_instance()
959 target_vcs = pull_request.target_repo.scm_instance()
960 source_vcs = pull_request.source_repo.scm_instance()
960 source_vcs = pull_request.source_repo.scm_instance()
961
961
962 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
962 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
963 pr_id=pull_request.pull_request_id,
963 pr_id=pull_request.pull_request_id,
964 pr_title=pull_request.title,
964 pr_title=pull_request.title,
965 pr_desc=pull_request.description,
965 pr_desc=pull_request.description,
966 source_repo=source_vcs.name,
966 source_repo=source_vcs.name,
967 source_ref_name=pull_request.source_ref_parts.name,
967 source_ref_name=pull_request.source_ref_parts.name,
968 target_repo=target_vcs.name,
968 target_repo=target_vcs.name,
969 target_ref_name=pull_request.target_ref_parts.name,
969 target_ref_name=pull_request.target_ref_parts.name,
970 )
970 )
971
971
972 workspace_id = self._workspace_id(pull_request)
972 workspace_id = self._workspace_id(pull_request)
973 repo_id = pull_request.target_repo.repo_id
973 repo_id = pull_request.target_repo.repo_id
974 use_rebase = self._use_rebase_for_merging(pull_request)
974 use_rebase = self._use_rebase_for_merging(pull_request)
975 close_branch = self._close_branch_before_merging(pull_request)
975 close_branch = self._close_branch_before_merging(pull_request)
976 user_name = self._user_name_for_merging(pull_request, user)
976 user_name = self._user_name_for_merging(pull_request, user)
977
977
978 target_ref = self._refresh_reference(
978 target_ref = self._refresh_reference(
979 pull_request.target_ref_parts, target_vcs)
979 pull_request.target_ref_parts, target_vcs)
980
980
981 callback_daemon, extras = prepare_callback_daemon(
981 callback_daemon, extras = prepare_callback_daemon(
982 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
982 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
983 host=vcs_settings.HOOKS_HOST,
983 host=vcs_settings.HOOKS_HOST,
984 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
984 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
985
985
986 with callback_daemon:
986 with callback_daemon:
987 # TODO: johbo: Implement a clean way to run a config_override
987 # TODO: johbo: Implement a clean way to run a config_override
988 # for a single call.
988 # for a single call.
989 target_vcs.config.set(
989 target_vcs.config.set(
990 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
990 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
991
991
992 merge_state = target_vcs.merge(
992 merge_state = target_vcs.merge(
993 repo_id, workspace_id, target_ref, source_vcs,
993 repo_id, workspace_id, target_ref, source_vcs,
994 pull_request.source_ref_parts,
994 pull_request.source_ref_parts,
995 user_name=user_name, user_email=user.email,
995 user_name=user_name, user_email=user.email,
996 message=message, use_rebase=use_rebase,
996 message=message, use_rebase=use_rebase,
997 close_branch=close_branch)
997 close_branch=close_branch)
998 return merge_state
998 return merge_state
999
999
1000 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
1000 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
1001 pull_request.merge_rev = merge_state.merge_ref.commit_id
1001 pull_request.merge_rev = merge_state.merge_ref.commit_id
1002 pull_request.updated_on = datetime.datetime.now()
1002 pull_request.updated_on = datetime.datetime.now()
1003 close_msg = close_msg or 'Pull request merged and closed'
1003 close_msg = close_msg or 'Pull request merged and closed'
1004
1004
1005 CommentsModel().create(
1005 CommentsModel().create(
1006 text=safe_unicode(close_msg),
1006 text=safe_unicode(close_msg),
1007 repo=pull_request.target_repo.repo_id,
1007 repo=pull_request.target_repo.repo_id,
1008 user=user.user_id,
1008 user=user.user_id,
1009 pull_request=pull_request.pull_request_id,
1009 pull_request=pull_request.pull_request_id,
1010 f_path=None,
1010 f_path=None,
1011 line_no=None,
1011 line_no=None,
1012 closing_pr=True
1012 closing_pr=True
1013 )
1013 )
1014
1014
1015 Session().add(pull_request)
1015 Session().add(pull_request)
1016 Session().flush()
1016 Session().flush()
1017 # TODO: paris: replace invalidation with less radical solution
1017 # TODO: paris: replace invalidation with less radical solution
1018 ScmModel().mark_for_invalidation(
1018 ScmModel().mark_for_invalidation(
1019 pull_request.target_repo.repo_name)
1019 pull_request.target_repo.repo_name)
1020 self.trigger_pull_request_hook(pull_request, user, 'merge')
1020 self.trigger_pull_request_hook(pull_request, user, 'merge')
1021
1021
1022 def has_valid_update_type(self, pull_request):
1022 def has_valid_update_type(self, pull_request):
1023 source_ref_type = pull_request.source_ref_parts.type
1023 source_ref_type = pull_request.source_ref_parts.type
1024 return source_ref_type in self.REF_TYPES
1024 return source_ref_type in self.REF_TYPES
1025
1025
1026 def get_flow_commits(self, pull_request):
1026 def get_flow_commits(self, pull_request):
1027
1027
1028 # source repo
1028 # source repo
1029 source_ref_name = pull_request.source_ref_parts.name
1029 source_ref_name = pull_request.source_ref_parts.name
1030 source_ref_type = pull_request.source_ref_parts.type
1030 source_ref_type = pull_request.source_ref_parts.type
1031 source_ref_id = pull_request.source_ref_parts.commit_id
1031 source_ref_id = pull_request.source_ref_parts.commit_id
1032 source_repo = pull_request.source_repo.scm_instance()
1032 source_repo = pull_request.source_repo.scm_instance()
1033
1033
1034 try:
1034 try:
1035 if source_ref_type in self.REF_TYPES:
1035 if source_ref_type in self.REF_TYPES:
1036 source_commit = source_repo.get_commit(
1036 source_commit = source_repo.get_commit(
1037 source_ref_name, reference_obj=pull_request.source_ref_parts)
1037 source_ref_name, reference_obj=pull_request.source_ref_parts)
1038 else:
1038 else:
1039 source_commit = source_repo.get_commit(source_ref_id)
1039 source_commit = source_repo.get_commit(source_ref_id)
1040 except CommitDoesNotExistError:
1040 except CommitDoesNotExistError:
1041 raise SourceRefMissing()
1041 raise SourceRefMissing()
1042
1042
1043 # target repo
1043 # target repo
1044 target_ref_name = pull_request.target_ref_parts.name
1044 target_ref_name = pull_request.target_ref_parts.name
1045 target_ref_type = pull_request.target_ref_parts.type
1045 target_ref_type = pull_request.target_ref_parts.type
1046 target_ref_id = pull_request.target_ref_parts.commit_id
1046 target_ref_id = pull_request.target_ref_parts.commit_id
1047 target_repo = pull_request.target_repo.scm_instance()
1047 target_repo = pull_request.target_repo.scm_instance()
1048
1048
1049 try:
1049 try:
1050 if target_ref_type in self.REF_TYPES:
1050 if target_ref_type in self.REF_TYPES:
1051 target_commit = target_repo.get_commit(
1051 target_commit = target_repo.get_commit(
1052 target_ref_name, reference_obj=pull_request.target_ref_parts)
1052 target_ref_name, reference_obj=pull_request.target_ref_parts)
1053 else:
1053 else:
1054 target_commit = target_repo.get_commit(target_ref_id)
1054 target_commit = target_repo.get_commit(target_ref_id)
1055 except CommitDoesNotExistError:
1055 except CommitDoesNotExistError:
1056 raise TargetRefMissing()
1056 raise TargetRefMissing()
1057
1057
1058 return source_commit, target_commit
1058 return source_commit, target_commit
1059
1059
1060 def update_commits(self, pull_request, updating_user):
1060 def update_commits(self, pull_request, updating_user):
1061 """
1061 """
1062 Get the updated list of commits for the pull request
1062 Get the updated list of commits for the pull request
1063 and return the new pull request version and the list
1063 and return the new pull request version and the list
1064 of commits processed by this update action
1064 of commits processed by this update action
1065
1065
1066 updating_user is the user_object who triggered the update
1066 updating_user is the user_object who triggered the update
1067 """
1067 """
1068 pull_request = self.__get_pull_request(pull_request)
1068 pull_request = self.__get_pull_request(pull_request)
1069 source_ref_type = pull_request.source_ref_parts.type
1069 source_ref_type = pull_request.source_ref_parts.type
1070 source_ref_name = pull_request.source_ref_parts.name
1070 source_ref_name = pull_request.source_ref_parts.name
1071 source_ref_id = pull_request.source_ref_parts.commit_id
1071 source_ref_id = pull_request.source_ref_parts.commit_id
1072
1072
1073 target_ref_type = pull_request.target_ref_parts.type
1073 target_ref_type = pull_request.target_ref_parts.type
1074 target_ref_name = pull_request.target_ref_parts.name
1074 target_ref_name = pull_request.target_ref_parts.name
1075 target_ref_id = pull_request.target_ref_parts.commit_id
1075 target_ref_id = pull_request.target_ref_parts.commit_id
1076
1076
1077 if not self.has_valid_update_type(pull_request):
1077 if not self.has_valid_update_type(pull_request):
1078 log.debug("Skipping update of pull request %s due to ref type: %s",
1078 log.debug("Skipping update of pull request %s due to ref type: %s",
1079 pull_request, source_ref_type)
1079 pull_request, source_ref_type)
1080 return UpdateResponse(
1080 return UpdateResponse(
1081 executed=False,
1081 executed=False,
1082 reason=UpdateFailureReason.WRONG_REF_TYPE,
1082 reason=UpdateFailureReason.WRONG_REF_TYPE,
1083 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1083 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1084 source_changed=False, target_changed=False)
1084 source_changed=False, target_changed=False)
1085
1085
1086 try:
1086 try:
1087 source_commit, target_commit = self.get_flow_commits(pull_request)
1087 source_commit, target_commit = self.get_flow_commits(pull_request)
1088 except SourceRefMissing:
1088 except SourceRefMissing:
1089 return UpdateResponse(
1089 return UpdateResponse(
1090 executed=False,
1090 executed=False,
1091 reason=UpdateFailureReason.MISSING_SOURCE_REF,
1091 reason=UpdateFailureReason.MISSING_SOURCE_REF,
1092 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1092 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1093 source_changed=False, target_changed=False)
1093 source_changed=False, target_changed=False)
1094 except TargetRefMissing:
1094 except TargetRefMissing:
1095 return UpdateResponse(
1095 return UpdateResponse(
1096 executed=False,
1096 executed=False,
1097 reason=UpdateFailureReason.MISSING_TARGET_REF,
1097 reason=UpdateFailureReason.MISSING_TARGET_REF,
1098 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1098 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1099 source_changed=False, target_changed=False)
1099 source_changed=False, target_changed=False)
1100
1100
1101 source_changed = source_ref_id != source_commit.raw_id
1101 source_changed = source_ref_id != source_commit.raw_id
1102 target_changed = target_ref_id != target_commit.raw_id
1102 target_changed = target_ref_id != target_commit.raw_id
1103
1103
1104 if not (source_changed or target_changed):
1104 if not (source_changed or target_changed):
1105 log.debug("Nothing changed in pull request %s", pull_request)
1105 log.debug("Nothing changed in pull request %s", pull_request)
1106 return UpdateResponse(
1106 return UpdateResponse(
1107 executed=False,
1107 executed=False,
1108 reason=UpdateFailureReason.NO_CHANGE,
1108 reason=UpdateFailureReason.NO_CHANGE,
1109 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1109 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1110 source_changed=target_changed, target_changed=source_changed)
1110 source_changed=target_changed, target_changed=source_changed)
1111
1111
1112 change_in_found = 'target repo' if target_changed else 'source repo'
1112 change_in_found = 'target repo' if target_changed else 'source repo'
1113 log.debug('Updating pull request because of change in %s detected',
1113 log.debug('Updating pull request because of change in %s detected',
1114 change_in_found)
1114 change_in_found)
1115
1115
1116 # Finally there is a need for an update, in case of source change
1116 # Finally there is a need for an update, in case of source change
1117 # we create a new version, else just an update
1117 # we create a new version, else just an update
1118 if source_changed:
1118 if source_changed:
1119 pull_request_version = self._create_version_from_snapshot(pull_request)
1119 pull_request_version = self._create_version_from_snapshot(pull_request)
1120 self._link_comments_to_version(pull_request_version)
1120 self._link_comments_to_version(pull_request_version)
1121 else:
1121 else:
1122 try:
1122 try:
1123 ver = pull_request.versions[-1]
1123 ver = pull_request.versions[-1]
1124 except IndexError:
1124 except IndexError:
1125 ver = None
1125 ver = None
1126
1126
1127 pull_request.pull_request_version_id = \
1127 pull_request.pull_request_version_id = \
1128 ver.pull_request_version_id if ver else None
1128 ver.pull_request_version_id if ver else None
1129 pull_request_version = pull_request
1129 pull_request_version = pull_request
1130
1130
1131 source_repo = pull_request.source_repo.scm_instance()
1131 source_repo = pull_request.source_repo.scm_instance()
1132 target_repo = pull_request.target_repo.scm_instance()
1132 target_repo = pull_request.target_repo.scm_instance()
1133
1133
1134 # re-compute commit ids
1134 # re-compute commit ids
1135 old_commit_ids = pull_request.revisions
1135 old_commit_ids = pull_request.revisions
1136 pre_load = ["author", "date", "message", "branch"]
1136 pre_load = ["author", "date", "message", "branch"]
1137 commit_ranges = target_repo.compare(
1137 commit_ranges = target_repo.compare(
1138 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1138 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1139 pre_load=pre_load)
1139 pre_load=pre_load)
1140
1140
1141 target_ref = target_commit.raw_id
1141 target_ref = target_commit.raw_id
1142 source_ref = source_commit.raw_id
1142 source_ref = source_commit.raw_id
1143 ancestor_commit_id = target_repo.get_common_ancestor(
1143 ancestor_commit_id = target_repo.get_common_ancestor(
1144 target_ref, source_ref, source_repo)
1144 target_ref, source_ref, source_repo)
1145
1145
1146 if not ancestor_commit_id:
1146 if not ancestor_commit_id:
1147 raise ValueError(
1147 raise ValueError(
1148 'cannot calculate diff info without a common ancestor. '
1148 'cannot calculate diff info without a common ancestor. '
1149 'Make sure both repositories are related, and have a common forking commit.')
1149 'Make sure both repositories are related, and have a common forking commit.')
1150
1150
1151 pull_request.common_ancestor_id = ancestor_commit_id
1151 pull_request.common_ancestor_id = ancestor_commit_id
1152
1152
1153 pull_request.source_ref = '%s:%s:%s' % (
1153 pull_request.source_ref = '%s:%s:%s' % (
1154 source_ref_type, source_ref_name, source_commit.raw_id)
1154 source_ref_type, source_ref_name, source_commit.raw_id)
1155 pull_request.target_ref = '%s:%s:%s' % (
1155 pull_request.target_ref = '%s:%s:%s' % (
1156 target_ref_type, target_ref_name, ancestor_commit_id)
1156 target_ref_type, target_ref_name, ancestor_commit_id)
1157
1157
1158 pull_request.revisions = [
1158 pull_request.revisions = [
1159 commit.raw_id for commit in reversed(commit_ranges)]
1159 commit.raw_id for commit in reversed(commit_ranges)]
1160 pull_request.updated_on = datetime.datetime.now()
1160 pull_request.updated_on = datetime.datetime.now()
1161 Session().add(pull_request)
1161 Session().add(pull_request)
1162 new_commit_ids = pull_request.revisions
1162 new_commit_ids = pull_request.revisions
1163
1163
1164 old_diff_data, new_diff_data = self._generate_update_diffs(
1164 old_diff_data, new_diff_data = self._generate_update_diffs(
1165 pull_request, pull_request_version)
1165 pull_request, pull_request_version)
1166
1166
1167 # calculate commit and file changes
1167 # calculate commit and file changes
1168 commit_changes = self._calculate_commit_id_changes(
1168 commit_changes = self._calculate_commit_id_changes(
1169 old_commit_ids, new_commit_ids)
1169 old_commit_ids, new_commit_ids)
1170 file_changes = self._calculate_file_changes(
1170 file_changes = self._calculate_file_changes(
1171 old_diff_data, new_diff_data)
1171 old_diff_data, new_diff_data)
1172
1172
1173 # set comments as outdated if DIFFS changed
1173 # set comments as outdated if DIFFS changed
1174 CommentsModel().outdate_comments(
1174 CommentsModel().outdate_comments(
1175 pull_request, old_diff_data=old_diff_data,
1175 pull_request, old_diff_data=old_diff_data,
1176 new_diff_data=new_diff_data)
1176 new_diff_data=new_diff_data)
1177
1177
1178 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1178 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1179 file_node_changes = (
1179 file_node_changes = (
1180 file_changes.added or file_changes.modified or file_changes.removed)
1180 file_changes.added or file_changes.modified or file_changes.removed)
1181 pr_has_changes = valid_commit_changes or file_node_changes
1181 pr_has_changes = valid_commit_changes or file_node_changes
1182
1182
1183 # Add an automatic comment to the pull request, in case
1183 # Add an automatic comment to the pull request, in case
1184 # anything has changed
1184 # anything has changed
1185 if pr_has_changes:
1185 if pr_has_changes:
1186 update_comment = CommentsModel().create(
1186 update_comment = CommentsModel().create(
1187 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1187 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1188 repo=pull_request.target_repo,
1188 repo=pull_request.target_repo,
1189 user=pull_request.author,
1189 user=pull_request.author,
1190 pull_request=pull_request,
1190 pull_request=pull_request,
1191 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1191 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1192
1192
1193 # Update status to "Under Review" for added commits
1193 # Update status to "Under Review" for added commits
1194 for commit_id in commit_changes.added:
1194 for commit_id in commit_changes.added:
1195 ChangesetStatusModel().set_status(
1195 ChangesetStatusModel().set_status(
1196 repo=pull_request.source_repo,
1196 repo=pull_request.source_repo,
1197 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1197 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1198 comment=update_comment,
1198 comment=update_comment,
1199 user=pull_request.author,
1199 user=pull_request.author,
1200 pull_request=pull_request,
1200 pull_request=pull_request,
1201 revision=commit_id)
1201 revision=commit_id)
1202
1202
1203 # initial commit
1203 # initial commit
1204 Session().commit()
1204 Session().commit()
1205
1205
1206 if pr_has_changes:
1206 if pr_has_changes:
1207 # send update email to users
1207 # send update email to users
1208 try:
1208 try:
1209 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1209 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1210 ancestor_commit_id=ancestor_commit_id,
1210 ancestor_commit_id=ancestor_commit_id,
1211 commit_changes=commit_changes,
1211 commit_changes=commit_changes,
1212 file_changes=file_changes)
1212 file_changes=file_changes)
1213 Session().commit()
1213 Session().commit()
1214 except Exception:
1214 except Exception:
1215 log.exception('Failed to send email notification to users')
1215 log.exception('Failed to send email notification to users')
1216 Session().rollback()
1216 Session().rollback()
1217
1217
1218 log.debug(
1218 log.debug(
1219 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1219 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1220 'removed_ids: %s', pull_request.pull_request_id,
1220 'removed_ids: %s', pull_request.pull_request_id,
1221 commit_changes.added, commit_changes.common, commit_changes.removed)
1221 commit_changes.added, commit_changes.common, commit_changes.removed)
1222 log.debug(
1222 log.debug(
1223 'Updated pull request with the following file changes: %s',
1223 'Updated pull request with the following file changes: %s',
1224 file_changes)
1224 file_changes)
1225
1225
1226 log.info(
1226 log.info(
1227 "Updated pull request %s from commit %s to commit %s, "
1227 "Updated pull request %s from commit %s to commit %s, "
1228 "stored new version %s of this pull request.",
1228 "stored new version %s of this pull request.",
1229 pull_request.pull_request_id, source_ref_id,
1229 pull_request.pull_request_id, source_ref_id,
1230 pull_request.source_ref_parts.commit_id,
1230 pull_request.source_ref_parts.commit_id,
1231 pull_request_version.pull_request_version_id)
1231 pull_request_version.pull_request_version_id)
1232
1232
1233 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1233 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1234
1234
1235 return UpdateResponse(
1235 return UpdateResponse(
1236 executed=True, reason=UpdateFailureReason.NONE,
1236 executed=True, reason=UpdateFailureReason.NONE,
1237 old=pull_request, new=pull_request_version,
1237 old=pull_request, new=pull_request_version,
1238 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1238 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1239 source_changed=source_changed, target_changed=target_changed)
1239 source_changed=source_changed, target_changed=target_changed)
1240
1240
1241 def _create_version_from_snapshot(self, pull_request):
1241 def _create_version_from_snapshot(self, pull_request):
1242 version = PullRequestVersion()
1242 version = PullRequestVersion()
1243 version.title = pull_request.title
1243 version.title = pull_request.title
1244 version.description = pull_request.description
1244 version.description = pull_request.description
1245 version.status = pull_request.status
1245 version.status = pull_request.status
1246 version.pull_request_state = pull_request.pull_request_state
1246 version.pull_request_state = pull_request.pull_request_state
1247 version.created_on = datetime.datetime.now()
1247 version.created_on = datetime.datetime.now()
1248 version.updated_on = pull_request.updated_on
1248 version.updated_on = pull_request.updated_on
1249 version.user_id = pull_request.user_id
1249 version.user_id = pull_request.user_id
1250 version.source_repo = pull_request.source_repo
1250 version.source_repo = pull_request.source_repo
1251 version.source_ref = pull_request.source_ref
1251 version.source_ref = pull_request.source_ref
1252 version.target_repo = pull_request.target_repo
1252 version.target_repo = pull_request.target_repo
1253 version.target_ref = pull_request.target_ref
1253 version.target_ref = pull_request.target_ref
1254
1254
1255 version._last_merge_source_rev = pull_request._last_merge_source_rev
1255 version._last_merge_source_rev = pull_request._last_merge_source_rev
1256 version._last_merge_target_rev = pull_request._last_merge_target_rev
1256 version._last_merge_target_rev = pull_request._last_merge_target_rev
1257 version.last_merge_status = pull_request.last_merge_status
1257 version.last_merge_status = pull_request.last_merge_status
1258 version.last_merge_metadata = pull_request.last_merge_metadata
1258 version.last_merge_metadata = pull_request.last_merge_metadata
1259 version.shadow_merge_ref = pull_request.shadow_merge_ref
1259 version.shadow_merge_ref = pull_request.shadow_merge_ref
1260 version.merge_rev = pull_request.merge_rev
1260 version.merge_rev = pull_request.merge_rev
1261 version.reviewer_data = pull_request.reviewer_data
1261 version.reviewer_data = pull_request.reviewer_data
1262
1262
1263 version.revisions = pull_request.revisions
1263 version.revisions = pull_request.revisions
1264 version.common_ancestor_id = pull_request.common_ancestor_id
1264 version.common_ancestor_id = pull_request.common_ancestor_id
1265 version.pull_request = pull_request
1265 version.pull_request = pull_request
1266 Session().add(version)
1266 Session().add(version)
1267 Session().flush()
1267 Session().flush()
1268
1268
1269 return version
1269 return version
1270
1270
1271 def _generate_update_diffs(self, pull_request, pull_request_version):
1271 def _generate_update_diffs(self, pull_request, pull_request_version):
1272
1272
1273 diff_context = (
1273 diff_context = (
1274 self.DIFF_CONTEXT +
1274 self.DIFF_CONTEXT +
1275 CommentsModel.needed_extra_diff_context())
1275 CommentsModel.needed_extra_diff_context())
1276 hide_whitespace_changes = False
1276 hide_whitespace_changes = False
1277 source_repo = pull_request_version.source_repo
1277 source_repo = pull_request_version.source_repo
1278 source_ref_id = pull_request_version.source_ref_parts.commit_id
1278 source_ref_id = pull_request_version.source_ref_parts.commit_id
1279 target_ref_id = pull_request_version.target_ref_parts.commit_id
1279 target_ref_id = pull_request_version.target_ref_parts.commit_id
1280 old_diff = self._get_diff_from_pr_or_version(
1280 old_diff = self._get_diff_from_pr_or_version(
1281 source_repo, source_ref_id, target_ref_id,
1281 source_repo, source_ref_id, target_ref_id,
1282 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1282 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1283
1283
1284 source_repo = pull_request.source_repo
1284 source_repo = pull_request.source_repo
1285 source_ref_id = pull_request.source_ref_parts.commit_id
1285 source_ref_id = pull_request.source_ref_parts.commit_id
1286 target_ref_id = pull_request.target_ref_parts.commit_id
1286 target_ref_id = pull_request.target_ref_parts.commit_id
1287
1287
1288 new_diff = self._get_diff_from_pr_or_version(
1288 new_diff = self._get_diff_from_pr_or_version(
1289 source_repo, source_ref_id, target_ref_id,
1289 source_repo, source_ref_id, target_ref_id,
1290 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1290 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1291
1291
1292 old_diff_data = diffs.DiffProcessor(old_diff)
1292 old_diff_data = diffs.DiffProcessor(old_diff)
1293 old_diff_data.prepare()
1293 old_diff_data.prepare()
1294 new_diff_data = diffs.DiffProcessor(new_diff)
1294 new_diff_data = diffs.DiffProcessor(new_diff)
1295 new_diff_data.prepare()
1295 new_diff_data.prepare()
1296
1296
1297 return old_diff_data, new_diff_data
1297 return old_diff_data, new_diff_data
1298
1298
1299 def _link_comments_to_version(self, pull_request_version):
1299 def _link_comments_to_version(self, pull_request_version):
1300 """
1300 """
1301 Link all unlinked comments of this pull request to the given version.
1301 Link all unlinked comments of this pull request to the given version.
1302
1302
1303 :param pull_request_version: The `PullRequestVersion` to which
1303 :param pull_request_version: The `PullRequestVersion` to which
1304 the comments shall be linked.
1304 the comments shall be linked.
1305
1305
1306 """
1306 """
1307 pull_request = pull_request_version.pull_request
1307 pull_request = pull_request_version.pull_request
1308 comments = ChangesetComment.query()\
1308 comments = ChangesetComment.query()\
1309 .filter(
1309 .filter(
1310 # TODO: johbo: Should we query for the repo at all here?
1310 # TODO: johbo: Should we query for the repo at all here?
1311 # Pending decision on how comments of PRs are to be related
1311 # Pending decision on how comments of PRs are to be related
1312 # to either the source repo, the target repo or no repo at all.
1312 # to either the source repo, the target repo or no repo at all.
1313 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1313 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1314 ChangesetComment.pull_request == pull_request,
1314 ChangesetComment.pull_request == pull_request,
1315 ChangesetComment.pull_request_version == None)\
1315 ChangesetComment.pull_request_version == None)\
1316 .order_by(ChangesetComment.comment_id.asc())
1316 .order_by(ChangesetComment.comment_id.asc())
1317
1317
1318 # TODO: johbo: Find out why this breaks if it is done in a bulk
1318 # TODO: johbo: Find out why this breaks if it is done in a bulk
1319 # operation.
1319 # operation.
1320 for comment in comments:
1320 for comment in comments:
1321 comment.pull_request_version_id = (
1321 comment.pull_request_version_id = (
1322 pull_request_version.pull_request_version_id)
1322 pull_request_version.pull_request_version_id)
1323 Session().add(comment)
1323 Session().add(comment)
1324
1324
1325 def _calculate_commit_id_changes(self, old_ids, new_ids):
1325 def _calculate_commit_id_changes(self, old_ids, new_ids):
1326 added = [x for x in new_ids if x not in old_ids]
1326 added = [x for x in new_ids if x not in old_ids]
1327 common = [x for x in new_ids if x in old_ids]
1327 common = [x for x in new_ids if x in old_ids]
1328 removed = [x for x in old_ids if x not in new_ids]
1328 removed = [x for x in old_ids if x not in new_ids]
1329 total = new_ids
1329 total = new_ids
1330 return ChangeTuple(added, common, removed, total)
1330 return ChangeTuple(added, common, removed, total)
1331
1331
1332 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1332 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1333
1333
1334 old_files = OrderedDict()
1334 old_files = OrderedDict()
1335 for diff_data in old_diff_data.parsed_diff:
1335 for diff_data in old_diff_data.parsed_diff:
1336 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1336 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1337
1337
1338 added_files = []
1338 added_files = []
1339 modified_files = []
1339 modified_files = []
1340 removed_files = []
1340 removed_files = []
1341 for diff_data in new_diff_data.parsed_diff:
1341 for diff_data in new_diff_data.parsed_diff:
1342 new_filename = diff_data['filename']
1342 new_filename = diff_data['filename']
1343 new_hash = md5_safe(diff_data['raw_diff'])
1343 new_hash = md5_safe(diff_data['raw_diff'])
1344
1344
1345 old_hash = old_files.get(new_filename)
1345 old_hash = old_files.get(new_filename)
1346 if not old_hash:
1346 if not old_hash:
1347 # file is not present in old diff, we have to figure out from parsed diff
1347 # file is not present in old diff, we have to figure out from parsed diff
1348 # operation ADD/REMOVE
1348 # operation ADD/REMOVE
1349 operations_dict = diff_data['stats']['ops']
1349 operations_dict = diff_data['stats']['ops']
1350 if diffs.DEL_FILENODE in operations_dict:
1350 if diffs.DEL_FILENODE in operations_dict:
1351 removed_files.append(new_filename)
1351 removed_files.append(new_filename)
1352 else:
1352 else:
1353 added_files.append(new_filename)
1353 added_files.append(new_filename)
1354 else:
1354 else:
1355 if new_hash != old_hash:
1355 if new_hash != old_hash:
1356 modified_files.append(new_filename)
1356 modified_files.append(new_filename)
1357 # now remove a file from old, since we have seen it already
1357 # now remove a file from old, since we have seen it already
1358 del old_files[new_filename]
1358 del old_files[new_filename]
1359
1359
1360 # removed files is when there are present in old, but not in NEW,
1360 # removed files is when there are present in old, but not in NEW,
1361 # since we remove old files that are present in new diff, left-overs
1361 # since we remove old files that are present in new diff, left-overs
1362 # if any should be the removed files
1362 # if any should be the removed files
1363 removed_files.extend(old_files.keys())
1363 removed_files.extend(old_files.keys())
1364
1364
1365 return FileChangeTuple(added_files, modified_files, removed_files)
1365 return FileChangeTuple(added_files, modified_files, removed_files)
1366
1366
1367 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1367 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1368 """
1368 """
1369 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1369 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1370 so it's always looking the same disregarding on which default
1370 so it's always looking the same disregarding on which default
1371 renderer system is using.
1371 renderer system is using.
1372
1372
1373 :param ancestor_commit_id: ancestor raw_id
1373 :param ancestor_commit_id: ancestor raw_id
1374 :param changes: changes named tuple
1374 :param changes: changes named tuple
1375 :param file_changes: file changes named tuple
1375 :param file_changes: file changes named tuple
1376
1376
1377 """
1377 """
1378 new_status = ChangesetStatus.get_status_lbl(
1378 new_status = ChangesetStatus.get_status_lbl(
1379 ChangesetStatus.STATUS_UNDER_REVIEW)
1379 ChangesetStatus.STATUS_UNDER_REVIEW)
1380
1380
1381 changed_files = (
1381 changed_files = (
1382 file_changes.added + file_changes.modified + file_changes.removed)
1382 file_changes.added + file_changes.modified + file_changes.removed)
1383
1383
1384 params = {
1384 params = {
1385 'under_review_label': new_status,
1385 'under_review_label': new_status,
1386 'added_commits': changes.added,
1386 'added_commits': changes.added,
1387 'removed_commits': changes.removed,
1387 'removed_commits': changes.removed,
1388 'changed_files': changed_files,
1388 'changed_files': changed_files,
1389 'added_files': file_changes.added,
1389 'added_files': file_changes.added,
1390 'modified_files': file_changes.modified,
1390 'modified_files': file_changes.modified,
1391 'removed_files': file_changes.removed,
1391 'removed_files': file_changes.removed,
1392 'ancestor_commit_id': ancestor_commit_id
1392 'ancestor_commit_id': ancestor_commit_id
1393 }
1393 }
1394 renderer = RstTemplateRenderer()
1394 renderer = RstTemplateRenderer()
1395 return renderer.render('pull_request_update.mako', **params)
1395 return renderer.render('pull_request_update.mako', **params)
1396
1396
1397 def edit(self, pull_request, title, description, description_renderer, user):
1397 def edit(self, pull_request, title, description, description_renderer, user):
1398 pull_request = self.__get_pull_request(pull_request)
1398 pull_request = self.__get_pull_request(pull_request)
1399 old_data = pull_request.get_api_data(with_merge_state=False)
1399 old_data = pull_request.get_api_data(with_merge_state=False)
1400 if pull_request.is_closed():
1400 if pull_request.is_closed():
1401 raise ValueError('This pull request is closed')
1401 raise ValueError('This pull request is closed')
1402 if title:
1402 if title:
1403 pull_request.title = title
1403 pull_request.title = title
1404 pull_request.description = description
1404 pull_request.description = description
1405 pull_request.updated_on = datetime.datetime.now()
1405 pull_request.updated_on = datetime.datetime.now()
1406 pull_request.description_renderer = description_renderer
1406 pull_request.description_renderer = description_renderer
1407 Session().add(pull_request)
1407 Session().add(pull_request)
1408 self._log_audit_action(
1408 self._log_audit_action(
1409 'repo.pull_request.edit', {'old_data': old_data},
1409 'repo.pull_request.edit', {'old_data': old_data},
1410 user, pull_request)
1410 user, pull_request)
1411
1411
1412 def update_reviewers(self, pull_request, reviewer_data, user):
1412 def update_reviewers(self, pull_request, reviewer_data, user):
1413 """
1413 """
1414 Update the reviewers in the pull request
1414 Update the reviewers in the pull request
1415
1415
1416 :param pull_request: the pr to update
1416 :param pull_request: the pr to update
1417 :param reviewer_data: list of tuples
1417 :param reviewer_data: list of tuples
1418 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1418 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1419 :param user: current use who triggers this action
1419 :param user: current use who triggers this action
1420 """
1420 """
1421
1421
1422 pull_request = self.__get_pull_request(pull_request)
1422 pull_request = self.__get_pull_request(pull_request)
1423 if pull_request.is_closed():
1423 if pull_request.is_closed():
1424 raise ValueError('This pull request is closed')
1424 raise ValueError('This pull request is closed')
1425
1425
1426 reviewers = {}
1426 reviewers = {}
1427 for user_id, reasons, mandatory, role, rules in reviewer_data:
1427 for user_id, reasons, mandatory, role, rules in reviewer_data:
1428 if isinstance(user_id, (int, str)):
1428 if isinstance(user_id, (int, str)):
1429 user_id = self._get_user(user_id).user_id
1429 user_id = self._get_user(user_id).user_id
1430 reviewers[user_id] = {
1430 reviewers[user_id] = {
1431 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1431 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1432
1432
1433 reviewers_ids = set(reviewers.keys())
1433 reviewers_ids = set(reviewers.keys())
1434 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1434 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1435 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1435 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1436
1436
1437 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1437 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1438
1438
1439 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1439 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1440 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1440 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1441
1441
1442 log.debug("Adding %s reviewers", ids_to_add)
1442 log.debug("Adding %s reviewers", ids_to_add)
1443 log.debug("Removing %s reviewers", ids_to_remove)
1443 log.debug("Removing %s reviewers", ids_to_remove)
1444 changed = False
1444 changed = False
1445 added_audit_reviewers = []
1445 added_audit_reviewers = []
1446 removed_audit_reviewers = []
1446 removed_audit_reviewers = []
1447
1447
1448 for uid in ids_to_add:
1448 for uid in ids_to_add:
1449 changed = True
1449 changed = True
1450 _usr = self._get_user(uid)
1450 _usr = self._get_user(uid)
1451 reviewer = PullRequestReviewers()
1451 reviewer = PullRequestReviewers()
1452 reviewer.user = _usr
1452 reviewer.user = _usr
1453 reviewer.pull_request = pull_request
1453 reviewer.pull_request = pull_request
1454 reviewer.reasons = reviewers[uid]['reasons']
1454 reviewer.reasons = reviewers[uid]['reasons']
1455 # NOTE(marcink): mandatory shouldn't be changed now
1455 # NOTE(marcink): mandatory shouldn't be changed now
1456 # reviewer.mandatory = reviewers[uid]['reasons']
1456 # reviewer.mandatory = reviewers[uid]['reasons']
1457 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1457 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1458 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1458 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1459 Session().add(reviewer)
1459 Session().add(reviewer)
1460 added_audit_reviewers.append(reviewer.get_dict())
1460 added_audit_reviewers.append(reviewer.get_dict())
1461
1461
1462 for uid in ids_to_remove:
1462 for uid in ids_to_remove:
1463 changed = True
1463 changed = True
1464 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1464 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1465 # This is an edge case that handles previous state of having the same reviewer twice.
1465 # This is an edge case that handles previous state of having the same reviewer twice.
1466 # this CAN happen due to the lack of DB checks
1466 # this CAN happen due to the lack of DB checks
1467 reviewers = PullRequestReviewers.query()\
1467 reviewers = PullRequestReviewers.query()\
1468 .filter(PullRequestReviewers.user_id == uid,
1468 .filter(PullRequestReviewers.user_id == uid,
1469 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1469 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1470 PullRequestReviewers.pull_request == pull_request)\
1470 PullRequestReviewers.pull_request == pull_request)\
1471 .all()
1471 .all()
1472
1472
1473 for obj in reviewers:
1473 for obj in reviewers:
1474 added_audit_reviewers.append(obj.get_dict())
1474 added_audit_reviewers.append(obj.get_dict())
1475 Session().delete(obj)
1475 Session().delete(obj)
1476
1476
1477 if changed:
1477 if changed:
1478 Session().expire_all()
1478 Session().expire_all()
1479 pull_request.updated_on = datetime.datetime.now()
1479 pull_request.updated_on = datetime.datetime.now()
1480 Session().add(pull_request)
1480 Session().add(pull_request)
1481
1481
1482 # finally store audit logs
1482 # finally store audit logs
1483 for user_data in added_audit_reviewers:
1483 for user_data in added_audit_reviewers:
1484 self._log_audit_action(
1484 self._log_audit_action(
1485 'repo.pull_request.reviewer.add', {'data': user_data},
1485 'repo.pull_request.reviewer.add', {'data': user_data},
1486 user, pull_request)
1486 user, pull_request)
1487 for user_data in removed_audit_reviewers:
1487 for user_data in removed_audit_reviewers:
1488 self._log_audit_action(
1488 self._log_audit_action(
1489 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1489 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1490 user, pull_request)
1490 user, pull_request)
1491
1491
1492 self.notify_reviewers(pull_request, ids_to_add, user)
1492 self.notify_reviewers(pull_request, ids_to_add, user)
1493 return ids_to_add, ids_to_remove
1493 return ids_to_add, ids_to_remove
1494
1494
1495 def update_observers(self, pull_request, observer_data, user):
1495 def update_observers(self, pull_request, observer_data, user):
1496 """
1496 """
1497 Update the observers in the pull request
1497 Update the observers in the pull request
1498
1498
1499 :param pull_request: the pr to update
1499 :param pull_request: the pr to update
1500 :param observer_data: list of tuples
1500 :param observer_data: list of tuples
1501 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1501 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1502 :param user: current use who triggers this action
1502 :param user: current use who triggers this action
1503 """
1503 """
1504 pull_request = self.__get_pull_request(pull_request)
1504 pull_request = self.__get_pull_request(pull_request)
1505 if pull_request.is_closed():
1505 if pull_request.is_closed():
1506 raise ValueError('This pull request is closed')
1506 raise ValueError('This pull request is closed')
1507
1507
1508 observers = {}
1508 observers = {}
1509 for user_id, reasons, mandatory, role, rules in observer_data:
1509 for user_id, reasons, mandatory, role, rules in observer_data:
1510 if isinstance(user_id, (int, str)):
1510 if isinstance(user_id, (int, str)):
1511 user_id = self._get_user(user_id).user_id
1511 user_id = self._get_user(user_id).user_id
1512 observers[user_id] = {
1512 observers[user_id] = {
1513 'reasons': reasons, 'observers': mandatory, 'role': role}
1513 'reasons': reasons, 'observers': mandatory, 'role': role}
1514
1514
1515 observers_ids = set(observers.keys())
1515 observers_ids = set(observers.keys())
1516 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1516 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1517 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1517 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1518
1518
1519 current_observers_ids = set([x.user.user_id for x in current_observers])
1519 current_observers_ids = set([x.user.user_id for x in current_observers])
1520
1520
1521 ids_to_add = observers_ids.difference(current_observers_ids)
1521 ids_to_add = observers_ids.difference(current_observers_ids)
1522 ids_to_remove = current_observers_ids.difference(observers_ids)
1522 ids_to_remove = current_observers_ids.difference(observers_ids)
1523
1523
1524 log.debug("Adding %s observer", ids_to_add)
1524 log.debug("Adding %s observer", ids_to_add)
1525 log.debug("Removing %s observer", ids_to_remove)
1525 log.debug("Removing %s observer", ids_to_remove)
1526 changed = False
1526 changed = False
1527 added_audit_observers = []
1527 added_audit_observers = []
1528 removed_audit_observers = []
1528 removed_audit_observers = []
1529
1529
1530 for uid in ids_to_add:
1530 for uid in ids_to_add:
1531 changed = True
1531 changed = True
1532 _usr = self._get_user(uid)
1532 _usr = self._get_user(uid)
1533 observer = PullRequestReviewers()
1533 observer = PullRequestReviewers()
1534 observer.user = _usr
1534 observer.user = _usr
1535 observer.pull_request = pull_request
1535 observer.pull_request = pull_request
1536 observer.reasons = observers[uid]['reasons']
1536 observer.reasons = observers[uid]['reasons']
1537 # NOTE(marcink): mandatory shouldn't be changed now
1537 # NOTE(marcink): mandatory shouldn't be changed now
1538 # observer.mandatory = observer[uid]['reasons']
1538 # observer.mandatory = observer[uid]['reasons']
1539
1539
1540 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1540 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1541 observer.role = PullRequestReviewers.ROLE_OBSERVER
1541 observer.role = PullRequestReviewers.ROLE_OBSERVER
1542 Session().add(observer)
1542 Session().add(observer)
1543 added_audit_observers.append(observer.get_dict())
1543 added_audit_observers.append(observer.get_dict())
1544
1544
1545 for uid in ids_to_remove:
1545 for uid in ids_to_remove:
1546 changed = True
1546 changed = True
1547 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1547 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1548 # This is an edge case that handles previous state of having the same reviewer twice.
1548 # This is an edge case that handles previous state of having the same reviewer twice.
1549 # this CAN happen due to the lack of DB checks
1549 # this CAN happen due to the lack of DB checks
1550 observers = PullRequestReviewers.query()\
1550 observers = PullRequestReviewers.query()\
1551 .filter(PullRequestReviewers.user_id == uid,
1551 .filter(PullRequestReviewers.user_id == uid,
1552 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1552 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1553 PullRequestReviewers.pull_request == pull_request)\
1553 PullRequestReviewers.pull_request == pull_request)\
1554 .all()
1554 .all()
1555
1555
1556 for obj in observers:
1556 for obj in observers:
1557 added_audit_observers.append(obj.get_dict())
1557 added_audit_observers.append(obj.get_dict())
1558 Session().delete(obj)
1558 Session().delete(obj)
1559
1559
1560 if changed:
1560 if changed:
1561 Session().expire_all()
1561 Session().expire_all()
1562 pull_request.updated_on = datetime.datetime.now()
1562 pull_request.updated_on = datetime.datetime.now()
1563 Session().add(pull_request)
1563 Session().add(pull_request)
1564
1564
1565 # finally store audit logs
1565 # finally store audit logs
1566 for user_data in added_audit_observers:
1566 for user_data in added_audit_observers:
1567 self._log_audit_action(
1567 self._log_audit_action(
1568 'repo.pull_request.observer.add', {'data': user_data},
1568 'repo.pull_request.observer.add', {'data': user_data},
1569 user, pull_request)
1569 user, pull_request)
1570 for user_data in removed_audit_observers:
1570 for user_data in removed_audit_observers:
1571 self._log_audit_action(
1571 self._log_audit_action(
1572 'repo.pull_request.observer.delete', {'old_data': user_data},
1572 'repo.pull_request.observer.delete', {'old_data': user_data},
1573 user, pull_request)
1573 user, pull_request)
1574
1574
1575 self.notify_observers(pull_request, ids_to_add, user)
1575 self.notify_observers(pull_request, ids_to_add, user)
1576 return ids_to_add, ids_to_remove
1576 return ids_to_add, ids_to_remove
1577
1577
1578 def get_url(self, pull_request, request=None, permalink=False):
1578 def get_url(self, pull_request, request=None, permalink=False):
1579 if not request:
1579 if not request:
1580 request = get_current_request()
1580 request = get_current_request()
1581
1581
1582 if permalink:
1582 if permalink:
1583 return request.route_url(
1583 return request.route_url(
1584 'pull_requests_global',
1584 'pull_requests_global',
1585 pull_request_id=pull_request.pull_request_id,)
1585 pull_request_id=pull_request.pull_request_id,)
1586 else:
1586 else:
1587 return request.route_url('pullrequest_show',
1587 return request.route_url('pullrequest_show',
1588 repo_name=safe_str(pull_request.target_repo.repo_name),
1588 repo_name=safe_str(pull_request.target_repo.repo_name),
1589 pull_request_id=pull_request.pull_request_id,)
1589 pull_request_id=pull_request.pull_request_id,)
1590
1590
1591 def get_shadow_clone_url(self, pull_request, request=None):
1591 def get_shadow_clone_url(self, pull_request, request=None):
1592 """
1592 """
1593 Returns qualified url pointing to the shadow repository. If this pull
1593 Returns qualified url pointing to the shadow repository. If this pull
1594 request is closed there is no shadow repository and ``None`` will be
1594 request is closed there is no shadow repository and ``None`` will be
1595 returned.
1595 returned.
1596 """
1596 """
1597 if pull_request.is_closed():
1597 if pull_request.is_closed():
1598 return None
1598 return None
1599 else:
1599 else:
1600 pr_url = urllib.parse.unquote(self.get_url(pull_request, request=request))
1600 pr_url = urllib.parse.unquote(self.get_url(pull_request, request=request))
1601 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1601 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1602
1602
1603 def _notify_reviewers(self, pull_request, user_ids, role, user):
1603 def _notify_reviewers(self, pull_request, user_ids, role, user):
1604 # notification to reviewers/observers
1604 # notification to reviewers/observers
1605 if not user_ids:
1605 if not user_ids:
1606 return
1606 return
1607
1607
1608 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1608 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1609
1609
1610 pull_request_obj = pull_request
1610 pull_request_obj = pull_request
1611 # get the current participants of this pull request
1611 # get the current participants of this pull request
1612 recipients = user_ids
1612 recipients = user_ids
1613 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1613 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1614
1614
1615 pr_source_repo = pull_request_obj.source_repo
1615 pr_source_repo = pull_request_obj.source_repo
1616 pr_target_repo = pull_request_obj.target_repo
1616 pr_target_repo = pull_request_obj.target_repo
1617
1617
1618 pr_url = h.route_url('pullrequest_show',
1618 pr_url = h.route_url('pullrequest_show',
1619 repo_name=pr_target_repo.repo_name,
1619 repo_name=pr_target_repo.repo_name,
1620 pull_request_id=pull_request_obj.pull_request_id,)
1620 pull_request_id=pull_request_obj.pull_request_id,)
1621
1621
1622 # set some variables for email notification
1622 # set some variables for email notification
1623 pr_target_repo_url = h.route_url(
1623 pr_target_repo_url = h.route_url(
1624 'repo_summary', repo_name=pr_target_repo.repo_name)
1624 'repo_summary', repo_name=pr_target_repo.repo_name)
1625
1625
1626 pr_source_repo_url = h.route_url(
1626 pr_source_repo_url = h.route_url(
1627 'repo_summary', repo_name=pr_source_repo.repo_name)
1627 'repo_summary', repo_name=pr_source_repo.repo_name)
1628
1628
1629 # pull request specifics
1629 # pull request specifics
1630 pull_request_commits = [
1630 pull_request_commits = [
1631 (x.raw_id, x.message)
1631 (x.raw_id, x.message)
1632 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1632 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1633
1633
1634 current_rhodecode_user = user
1634 current_rhodecode_user = user
1635 kwargs = {
1635 kwargs = {
1636 'user': current_rhodecode_user,
1636 'user': current_rhodecode_user,
1637 'pull_request_author': pull_request.author,
1637 'pull_request_author': pull_request.author,
1638 'pull_request': pull_request_obj,
1638 'pull_request': pull_request_obj,
1639 'pull_request_commits': pull_request_commits,
1639 'pull_request_commits': pull_request_commits,
1640
1640
1641 'pull_request_target_repo': pr_target_repo,
1641 'pull_request_target_repo': pr_target_repo,
1642 'pull_request_target_repo_url': pr_target_repo_url,
1642 'pull_request_target_repo_url': pr_target_repo_url,
1643
1643
1644 'pull_request_source_repo': pr_source_repo,
1644 'pull_request_source_repo': pr_source_repo,
1645 'pull_request_source_repo_url': pr_source_repo_url,
1645 'pull_request_source_repo_url': pr_source_repo_url,
1646
1646
1647 'pull_request_url': pr_url,
1647 'pull_request_url': pr_url,
1648 'thread_ids': [pr_url],
1648 'thread_ids': [pr_url],
1649 'user_role': role
1649 'user_role': role
1650 }
1650 }
1651
1651
1652 # create notification objects, and emails
1652 # create notification objects, and emails
1653 NotificationModel().create(
1653 NotificationModel().create(
1654 created_by=current_rhodecode_user,
1654 created_by=current_rhodecode_user,
1655 notification_subject='', # Filled in based on the notification_type
1655 notification_subject='', # Filled in based on the notification_type
1656 notification_body='', # Filled in based on the notification_type
1656 notification_body='', # Filled in based on the notification_type
1657 notification_type=notification_type,
1657 notification_type=notification_type,
1658 recipients=recipients,
1658 recipients=recipients,
1659 email_kwargs=kwargs,
1659 email_kwargs=kwargs,
1660 )
1660 )
1661
1661
1662 def notify_reviewers(self, pull_request, reviewers_ids, user):
1662 def notify_reviewers(self, pull_request, reviewers_ids, user):
1663 return self._notify_reviewers(pull_request, reviewers_ids,
1663 return self._notify_reviewers(pull_request, reviewers_ids,
1664 PullRequestReviewers.ROLE_REVIEWER, user)
1664 PullRequestReviewers.ROLE_REVIEWER, user)
1665
1665
1666 def notify_observers(self, pull_request, observers_ids, user):
1666 def notify_observers(self, pull_request, observers_ids, user):
1667 return self._notify_reviewers(pull_request, observers_ids,
1667 return self._notify_reviewers(pull_request, observers_ids,
1668 PullRequestReviewers.ROLE_OBSERVER, user)
1668 PullRequestReviewers.ROLE_OBSERVER, user)
1669
1669
1670 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1670 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1671 commit_changes, file_changes):
1671 commit_changes, file_changes):
1672
1672
1673 updating_user_id = updating_user.user_id
1673 updating_user_id = updating_user.user_id
1674 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1674 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1675 # NOTE(marcink): send notification to all other users except to
1675 # NOTE(marcink): send notification to all other users except to
1676 # person who updated the PR
1676 # person who updated the PR
1677 recipients = reviewers.difference(set([updating_user_id]))
1677 recipients = reviewers.difference(set([updating_user_id]))
1678
1678
1679 log.debug('Notify following recipients about pull-request update %s', recipients)
1679 log.debug('Notify following recipients about pull-request update %s', recipients)
1680
1680
1681 pull_request_obj = pull_request
1681 pull_request_obj = pull_request
1682
1682
1683 # send email about the update
1683 # send email about the update
1684 changed_files = (
1684 changed_files = (
1685 file_changes.added + file_changes.modified + file_changes.removed)
1685 file_changes.added + file_changes.modified + file_changes.removed)
1686
1686
1687 pr_source_repo = pull_request_obj.source_repo
1687 pr_source_repo = pull_request_obj.source_repo
1688 pr_target_repo = pull_request_obj.target_repo
1688 pr_target_repo = pull_request_obj.target_repo
1689
1689
1690 pr_url = h.route_url('pullrequest_show',
1690 pr_url = h.route_url('pullrequest_show',
1691 repo_name=pr_target_repo.repo_name,
1691 repo_name=pr_target_repo.repo_name,
1692 pull_request_id=pull_request_obj.pull_request_id,)
1692 pull_request_id=pull_request_obj.pull_request_id,)
1693
1693
1694 # set some variables for email notification
1694 # set some variables for email notification
1695 pr_target_repo_url = h.route_url(
1695 pr_target_repo_url = h.route_url(
1696 'repo_summary', repo_name=pr_target_repo.repo_name)
1696 'repo_summary', repo_name=pr_target_repo.repo_name)
1697
1697
1698 pr_source_repo_url = h.route_url(
1698 pr_source_repo_url = h.route_url(
1699 'repo_summary', repo_name=pr_source_repo.repo_name)
1699 'repo_summary', repo_name=pr_source_repo.repo_name)
1700
1700
1701 email_kwargs = {
1701 email_kwargs = {
1702 'date': datetime.datetime.now(),
1702 'date': datetime.datetime.now(),
1703 'updating_user': updating_user,
1703 'updating_user': updating_user,
1704
1704
1705 'pull_request': pull_request_obj,
1705 'pull_request': pull_request_obj,
1706
1706
1707 'pull_request_target_repo': pr_target_repo,
1707 'pull_request_target_repo': pr_target_repo,
1708 'pull_request_target_repo_url': pr_target_repo_url,
1708 'pull_request_target_repo_url': pr_target_repo_url,
1709
1709
1710 'pull_request_source_repo': pr_source_repo,
1710 'pull_request_source_repo': pr_source_repo,
1711 'pull_request_source_repo_url': pr_source_repo_url,
1711 'pull_request_source_repo_url': pr_source_repo_url,
1712
1712
1713 'pull_request_url': pr_url,
1713 'pull_request_url': pr_url,
1714
1714
1715 'ancestor_commit_id': ancestor_commit_id,
1715 'ancestor_commit_id': ancestor_commit_id,
1716 'added_commits': commit_changes.added,
1716 'added_commits': commit_changes.added,
1717 'removed_commits': commit_changes.removed,
1717 'removed_commits': commit_changes.removed,
1718 'changed_files': changed_files,
1718 'changed_files': changed_files,
1719 'added_files': file_changes.added,
1719 'added_files': file_changes.added,
1720 'modified_files': file_changes.modified,
1720 'modified_files': file_changes.modified,
1721 'removed_files': file_changes.removed,
1721 'removed_files': file_changes.removed,
1722 'thread_ids': [pr_url],
1722 'thread_ids': [pr_url],
1723 }
1723 }
1724
1724
1725 # create notification objects, and emails
1725 # create notification objects, and emails
1726 NotificationModel().create(
1726 NotificationModel().create(
1727 created_by=updating_user,
1727 created_by=updating_user,
1728 notification_subject='', # Filled in based on the notification_type
1728 notification_subject='', # Filled in based on the notification_type
1729 notification_body='', # Filled in based on the notification_type
1729 notification_body='', # Filled in based on the notification_type
1730 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1730 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1731 recipients=recipients,
1731 recipients=recipients,
1732 email_kwargs=email_kwargs,
1732 email_kwargs=email_kwargs,
1733 )
1733 )
1734
1734
1735 def delete(self, pull_request, user=None):
1735 def delete(self, pull_request, user=None):
1736 if not user:
1736 if not user:
1737 user = getattr(get_current_rhodecode_user(), 'username', None)
1737 user = getattr(get_current_rhodecode_user(), 'username', None)
1738
1738
1739 pull_request = self.__get_pull_request(pull_request)
1739 pull_request = self.__get_pull_request(pull_request)
1740 old_data = pull_request.get_api_data(with_merge_state=False)
1740 old_data = pull_request.get_api_data(with_merge_state=False)
1741 self._cleanup_merge_workspace(pull_request)
1741 self._cleanup_merge_workspace(pull_request)
1742 self._log_audit_action(
1742 self._log_audit_action(
1743 'repo.pull_request.delete', {'old_data': old_data},
1743 'repo.pull_request.delete', {'old_data': old_data},
1744 user, pull_request)
1744 user, pull_request)
1745 Session().delete(pull_request)
1745 Session().delete(pull_request)
1746
1746
1747 def close_pull_request(self, pull_request, user):
1747 def close_pull_request(self, pull_request, user):
1748 pull_request = self.__get_pull_request(pull_request)
1748 pull_request = self.__get_pull_request(pull_request)
1749 self._cleanup_merge_workspace(pull_request)
1749 self._cleanup_merge_workspace(pull_request)
1750 pull_request.status = PullRequest.STATUS_CLOSED
1750 pull_request.status = PullRequest.STATUS_CLOSED
1751 pull_request.updated_on = datetime.datetime.now()
1751 pull_request.updated_on = datetime.datetime.now()
1752 Session().add(pull_request)
1752 Session().add(pull_request)
1753 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1753 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1754
1754
1755 pr_data = pull_request.get_api_data(with_merge_state=False)
1755 pr_data = pull_request.get_api_data(with_merge_state=False)
1756 self._log_audit_action(
1756 self._log_audit_action(
1757 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1757 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1758
1758
1759 def close_pull_request_with_comment(
1759 def close_pull_request_with_comment(
1760 self, pull_request, user, repo, message=None, auth_user=None):
1760 self, pull_request, user, repo, message=None, auth_user=None):
1761
1761
1762 pull_request_review_status = pull_request.calculated_review_status()
1762 pull_request_review_status = pull_request.calculated_review_status()
1763
1763
1764 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1764 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1765 # approved only if we have voting consent
1765 # approved only if we have voting consent
1766 status = ChangesetStatus.STATUS_APPROVED
1766 status = ChangesetStatus.STATUS_APPROVED
1767 else:
1767 else:
1768 status = ChangesetStatus.STATUS_REJECTED
1768 status = ChangesetStatus.STATUS_REJECTED
1769 status_lbl = ChangesetStatus.get_status_lbl(status)
1769 status_lbl = ChangesetStatus.get_status_lbl(status)
1770
1770
1771 default_message = (
1771 default_message = (
1772 'Closing with status change {transition_icon} {status}.'
1772 'Closing with status change {transition_icon} {status}.'
1773 ).format(transition_icon='>', status=status_lbl)
1773 ).format(transition_icon='>', status=status_lbl)
1774 text = message or default_message
1774 text = message or default_message
1775
1775
1776 # create a comment, and link it to new status
1776 # create a comment, and link it to new status
1777 comment = CommentsModel().create(
1777 comment = CommentsModel().create(
1778 text=text,
1778 text=text,
1779 repo=repo.repo_id,
1779 repo=repo.repo_id,
1780 user=user.user_id,
1780 user=user.user_id,
1781 pull_request=pull_request.pull_request_id,
1781 pull_request=pull_request.pull_request_id,
1782 status_change=status_lbl,
1782 status_change=status_lbl,
1783 status_change_type=status,
1783 status_change_type=status,
1784 closing_pr=True,
1784 closing_pr=True,
1785 auth_user=auth_user,
1785 auth_user=auth_user,
1786 )
1786 )
1787
1787
1788 # calculate old status before we change it
1788 # calculate old status before we change it
1789 old_calculated_status = pull_request.calculated_review_status()
1789 old_calculated_status = pull_request.calculated_review_status()
1790 ChangesetStatusModel().set_status(
1790 ChangesetStatusModel().set_status(
1791 repo.repo_id,
1791 repo.repo_id,
1792 status,
1792 status,
1793 user.user_id,
1793 user.user_id,
1794 comment=comment,
1794 comment=comment,
1795 pull_request=pull_request.pull_request_id
1795 pull_request=pull_request.pull_request_id
1796 )
1796 )
1797
1797
1798 Session().flush()
1798 Session().flush()
1799
1799
1800 self.trigger_pull_request_hook(pull_request, user, 'comment',
1800 self.trigger_pull_request_hook(pull_request, user, 'comment',
1801 data={'comment': comment})
1801 data={'comment': comment})
1802
1802
1803 # we now calculate the status of pull request again, and based on that
1803 # we now calculate the status of pull request again, and based on that
1804 # calculation trigger status change. This might happen in cases
1804 # calculation trigger status change. This might happen in cases
1805 # that non-reviewer admin closes a pr, which means his vote doesn't
1805 # that non-reviewer admin closes a pr, which means his vote doesn't
1806 # change the status, while if he's a reviewer this might change it.
1806 # change the status, while if he's a reviewer this might change it.
1807 calculated_status = pull_request.calculated_review_status()
1807 calculated_status = pull_request.calculated_review_status()
1808 if old_calculated_status != calculated_status:
1808 if old_calculated_status != calculated_status:
1809 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1809 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1810 data={'status': calculated_status})
1810 data={'status': calculated_status})
1811
1811
1812 # finally close the PR
1812 # finally close the PR
1813 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1813 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1814
1814
1815 return comment, status
1815 return comment, status
1816
1816
1817 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1817 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1818 _ = translator or get_current_request().translate
1818 _ = translator or get_current_request().translate
1819
1819
1820 if not self._is_merge_enabled(pull_request):
1820 if not self._is_merge_enabled(pull_request):
1821 return None, False, _('Server-side pull request merging is disabled.')
1821 return None, False, _('Server-side pull request merging is disabled.')
1822
1822
1823 if pull_request.is_closed():
1823 if pull_request.is_closed():
1824 return None, False, _('This pull request is closed.')
1824 return None, False, _('This pull request is closed.')
1825
1825
1826 merge_possible, msg = self._check_repo_requirements(
1826 merge_possible, msg = self._check_repo_requirements(
1827 target=pull_request.target_repo, source=pull_request.source_repo,
1827 target=pull_request.target_repo, source=pull_request.source_repo,
1828 translator=_)
1828 translator=_)
1829 if not merge_possible:
1829 if not merge_possible:
1830 return None, merge_possible, msg
1830 return None, merge_possible, msg
1831
1831
1832 try:
1832 try:
1833 merge_response = self._try_merge(
1833 merge_response = self._try_merge(
1834 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1834 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1835 log.debug("Merge response: %s", merge_response)
1835 log.debug("Merge response: %s", merge_response)
1836 return merge_response, merge_response.possible, merge_response.merge_status_message
1836 return merge_response, merge_response.possible, merge_response.merge_status_message
1837 except NotImplementedError:
1837 except NotImplementedError:
1838 return None, False, _('Pull request merging is not supported.')
1838 return None, False, _('Pull request merging is not supported.')
1839
1839
1840 def _check_repo_requirements(self, target, source, translator):
1840 def _check_repo_requirements(self, target, source, translator):
1841 """
1841 """
1842 Check if `target` and `source` have compatible requirements.
1842 Check if `target` and `source` have compatible requirements.
1843
1843
1844 Currently this is just checking for largefiles.
1844 Currently this is just checking for largefiles.
1845 """
1845 """
1846 _ = translator
1846 _ = translator
1847 target_has_largefiles = self._has_largefiles(target)
1847 target_has_largefiles = self._has_largefiles(target)
1848 source_has_largefiles = self._has_largefiles(source)
1848 source_has_largefiles = self._has_largefiles(source)
1849 merge_possible = True
1849 merge_possible = True
1850 message = u''
1850 message = u''
1851
1851
1852 if target_has_largefiles != source_has_largefiles:
1852 if target_has_largefiles != source_has_largefiles:
1853 merge_possible = False
1853 merge_possible = False
1854 if source_has_largefiles:
1854 if source_has_largefiles:
1855 message = _(
1855 message = _(
1856 'Target repository large files support is disabled.')
1856 'Target repository large files support is disabled.')
1857 else:
1857 else:
1858 message = _(
1858 message = _(
1859 'Source repository large files support is disabled.')
1859 'Source repository large files support is disabled.')
1860
1860
1861 return merge_possible, message
1861 return merge_possible, message
1862
1862
1863 def _has_largefiles(self, repo):
1863 def _has_largefiles(self, repo):
1864 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1864 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1865 'extensions', 'largefiles')
1865 'extensions', 'largefiles')
1866 return largefiles_ui and largefiles_ui[0].active
1866 return largefiles_ui and largefiles_ui[0].active
1867
1867
1868 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1868 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1869 """
1869 """
1870 Try to merge the pull request and return the merge status.
1870 Try to merge the pull request and return the merge status.
1871 """
1871 """
1872 log.debug(
1872 log.debug(
1873 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1873 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1874 pull_request.pull_request_id, force_shadow_repo_refresh)
1874 pull_request.pull_request_id, force_shadow_repo_refresh)
1875 target_vcs = pull_request.target_repo.scm_instance()
1875 target_vcs = pull_request.target_repo.scm_instance()
1876 # Refresh the target reference.
1876 # Refresh the target reference.
1877 try:
1877 try:
1878 target_ref = self._refresh_reference(
1878 target_ref = self._refresh_reference(
1879 pull_request.target_ref_parts, target_vcs)
1879 pull_request.target_ref_parts, target_vcs)
1880 except CommitDoesNotExistError:
1880 except CommitDoesNotExistError:
1881 merge_state = MergeResponse(
1881 merge_state = MergeResponse(
1882 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1882 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1883 metadata={'target_ref': pull_request.target_ref_parts})
1883 metadata={'target_ref': pull_request.target_ref_parts})
1884 return merge_state
1884 return merge_state
1885
1885
1886 target_locked = pull_request.target_repo.locked
1886 target_locked = pull_request.target_repo.locked
1887 if target_locked and target_locked[0]:
1887 if target_locked and target_locked[0]:
1888 locked_by = 'user:{}'.format(target_locked[0])
1888 locked_by = 'user:{}'.format(target_locked[0])
1889 log.debug("The target repository is locked by %s.", locked_by)
1889 log.debug("The target repository is locked by %s.", locked_by)
1890 merge_state = MergeResponse(
1890 merge_state = MergeResponse(
1891 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1891 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1892 metadata={'locked_by': locked_by})
1892 metadata={'locked_by': locked_by})
1893 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1893 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1894 pull_request, target_ref):
1894 pull_request, target_ref):
1895 log.debug("Refreshing the merge status of the repository.")
1895 log.debug("Refreshing the merge status of the repository.")
1896 merge_state = self._refresh_merge_state(
1896 merge_state = self._refresh_merge_state(
1897 pull_request, target_vcs, target_ref)
1897 pull_request, target_vcs, target_ref)
1898 else:
1898 else:
1899 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1899 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1900 metadata = {
1900 metadata = {
1901 'unresolved_files': '',
1901 'unresolved_files': '',
1902 'target_ref': pull_request.target_ref_parts,
1902 'target_ref': pull_request.target_ref_parts,
1903 'source_ref': pull_request.source_ref_parts,
1903 'source_ref': pull_request.source_ref_parts,
1904 }
1904 }
1905 if pull_request.last_merge_metadata:
1905 if pull_request.last_merge_metadata:
1906 metadata.update(pull_request.last_merge_metadata_parsed)
1906 metadata.update(pull_request.last_merge_metadata_parsed)
1907
1907
1908 if not possible and target_ref.type == 'branch':
1908 if not possible and target_ref.type == 'branch':
1909 # NOTE(marcink): case for mercurial multiple heads on branch
1909 # NOTE(marcink): case for mercurial multiple heads on branch
1910 heads = target_vcs._heads(target_ref.name)
1910 heads = target_vcs._heads(target_ref.name)
1911 if len(heads) != 1:
1911 if len(heads) != 1:
1912 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1912 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1913 metadata.update({
1913 metadata.update({
1914 'heads': heads
1914 'heads': heads
1915 })
1915 })
1916
1916
1917 merge_state = MergeResponse(
1917 merge_state = MergeResponse(
1918 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1918 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1919
1919
1920 return merge_state
1920 return merge_state
1921
1921
1922 def _refresh_reference(self, reference, vcs_repository):
1922 def _refresh_reference(self, reference, vcs_repository):
1923 if reference.type in self.UPDATABLE_REF_TYPES:
1923 if reference.type in self.UPDATABLE_REF_TYPES:
1924 name_or_id = reference.name
1924 name_or_id = reference.name
1925 else:
1925 else:
1926 name_or_id = reference.commit_id
1926 name_or_id = reference.commit_id
1927
1927
1928 refreshed_commit = vcs_repository.get_commit(name_or_id)
1928 refreshed_commit = vcs_repository.get_commit(name_or_id)
1929 refreshed_reference = Reference(
1929 refreshed_reference = Reference(
1930 reference.type, reference.name, refreshed_commit.raw_id)
1930 reference.type, reference.name, refreshed_commit.raw_id)
1931 return refreshed_reference
1931 return refreshed_reference
1932
1932
1933 def _needs_merge_state_refresh(self, pull_request, target_reference):
1933 def _needs_merge_state_refresh(self, pull_request, target_reference):
1934 return not(
1934 return not(
1935 pull_request.revisions and
1935 pull_request.revisions and
1936 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1936 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1937 target_reference.commit_id == pull_request._last_merge_target_rev)
1937 target_reference.commit_id == pull_request._last_merge_target_rev)
1938
1938
1939 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1939 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1940 workspace_id = self._workspace_id(pull_request)
1940 workspace_id = self._workspace_id(pull_request)
1941 source_vcs = pull_request.source_repo.scm_instance()
1941 source_vcs = pull_request.source_repo.scm_instance()
1942 repo_id = pull_request.target_repo.repo_id
1942 repo_id = pull_request.target_repo.repo_id
1943 use_rebase = self._use_rebase_for_merging(pull_request)
1943 use_rebase = self._use_rebase_for_merging(pull_request)
1944 close_branch = self._close_branch_before_merging(pull_request)
1944 close_branch = self._close_branch_before_merging(pull_request)
1945 merge_state = target_vcs.merge(
1945 merge_state = target_vcs.merge(
1946 repo_id, workspace_id,
1946 repo_id, workspace_id,
1947 target_reference, source_vcs, pull_request.source_ref_parts,
1947 target_reference, source_vcs, pull_request.source_ref_parts,
1948 dry_run=True, use_rebase=use_rebase,
1948 dry_run=True, use_rebase=use_rebase,
1949 close_branch=close_branch)
1949 close_branch=close_branch)
1950
1950
1951 # Do not store the response if there was an unknown error.
1951 # Do not store the response if there was an unknown error.
1952 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1952 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1953 pull_request._last_merge_source_rev = \
1953 pull_request._last_merge_source_rev = \
1954 pull_request.source_ref_parts.commit_id
1954 pull_request.source_ref_parts.commit_id
1955 pull_request._last_merge_target_rev = target_reference.commit_id
1955 pull_request._last_merge_target_rev = target_reference.commit_id
1956 pull_request.last_merge_status = merge_state.failure_reason
1956 pull_request.last_merge_status = merge_state.failure_reason
1957 pull_request.last_merge_metadata = merge_state.metadata
1957 pull_request.last_merge_metadata = merge_state.metadata
1958
1958
1959 pull_request.shadow_merge_ref = merge_state.merge_ref
1959 pull_request.shadow_merge_ref = merge_state.merge_ref
1960 Session().add(pull_request)
1960 Session().add(pull_request)
1961 Session().commit()
1961 Session().commit()
1962
1962
1963 return merge_state
1963 return merge_state
1964
1964
1965 def _workspace_id(self, pull_request):
1965 def _workspace_id(self, pull_request):
1966 workspace_id = 'pr-%s' % pull_request.pull_request_id
1966 workspace_id = 'pr-%s' % pull_request.pull_request_id
1967 return workspace_id
1967 return workspace_id
1968
1968
1969 def generate_repo_data(self, repo, commit_id=None, branch=None,
1969 def generate_repo_data(self, repo, commit_id=None, branch=None,
1970 bookmark=None, translator=None):
1970 bookmark=None, translator=None):
1971 from rhodecode.model.repo import RepoModel
1971 from rhodecode.model.repo import RepoModel
1972
1972
1973 all_refs, selected_ref = \
1973 all_refs, selected_ref = \
1974 self._get_repo_pullrequest_sources(
1974 self._get_repo_pullrequest_sources(
1975 repo.scm_instance(), commit_id=commit_id,
1975 repo.scm_instance(), commit_id=commit_id,
1976 branch=branch, bookmark=bookmark, translator=translator)
1976 branch=branch, bookmark=bookmark, translator=translator)
1977
1977
1978 refs_select2 = []
1978 refs_select2 = []
1979 for element in all_refs:
1979 for element in all_refs:
1980 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1980 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1981 refs_select2.append({'text': element[1], 'children': children})
1981 refs_select2.append({'text': element[1], 'children': children})
1982
1982
1983 return {
1983 return {
1984 'user': {
1984 'user': {
1985 'user_id': repo.user.user_id,
1985 'user_id': repo.user.user_id,
1986 'username': repo.user.username,
1986 'username': repo.user.username,
1987 'firstname': repo.user.first_name,
1987 'firstname': repo.user.first_name,
1988 'lastname': repo.user.last_name,
1988 'lastname': repo.user.last_name,
1989 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1989 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1990 },
1990 },
1991 'name': repo.repo_name,
1991 'name': repo.repo_name,
1992 'link': RepoModel().get_url(repo),
1992 'link': RepoModel().get_url(repo),
1993 'description': h.chop_at_smart(repo.description_safe, '\n'),
1993 'description': h.chop_at_smart(repo.description_safe, '\n'),
1994 'refs': {
1994 'refs': {
1995 'all_refs': all_refs,
1995 'all_refs': all_refs,
1996 'selected_ref': selected_ref,
1996 'selected_ref': selected_ref,
1997 'select2_refs': refs_select2
1997 'select2_refs': refs_select2
1998 }
1998 }
1999 }
1999 }
2000
2000
2001 def generate_pullrequest_title(self, source, source_ref, target):
2001 def generate_pullrequest_title(self, source, source_ref, target):
2002 return u'{source}#{at_ref} to {target}'.format(
2002 return u'{source}#{at_ref} to {target}'.format(
2003 source=source,
2003 source=source,
2004 at_ref=source_ref,
2004 at_ref=source_ref,
2005 target=target,
2005 target=target,
2006 )
2006 )
2007
2007
2008 def _cleanup_merge_workspace(self, pull_request):
2008 def _cleanup_merge_workspace(self, pull_request):
2009 # Merging related cleanup
2009 # Merging related cleanup
2010 repo_id = pull_request.target_repo.repo_id
2010 repo_id = pull_request.target_repo.repo_id
2011 target_scm = pull_request.target_repo.scm_instance()
2011 target_scm = pull_request.target_repo.scm_instance()
2012 workspace_id = self._workspace_id(pull_request)
2012 workspace_id = self._workspace_id(pull_request)
2013
2013
2014 try:
2014 try:
2015 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
2015 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
2016 except NotImplementedError:
2016 except NotImplementedError:
2017 pass
2017 pass
2018
2018
2019 def _get_repo_pullrequest_sources(
2019 def _get_repo_pullrequest_sources(
2020 self, repo, commit_id=None, branch=None, bookmark=None,
2020 self, repo, commit_id=None, branch=None, bookmark=None,
2021 translator=None):
2021 translator=None):
2022 """
2022 """
2023 Return a structure with repo's interesting commits, suitable for
2023 Return a structure with repo's interesting commits, suitable for
2024 the selectors in pullrequest controller
2024 the selectors in pullrequest controller
2025
2025
2026 :param commit_id: a commit that must be in the list somehow
2026 :param commit_id: a commit that must be in the list somehow
2027 and selected by default
2027 and selected by default
2028 :param branch: a branch that must be in the list and selected
2028 :param branch: a branch that must be in the list and selected
2029 by default - even if closed
2029 by default - even if closed
2030 :param bookmark: a bookmark that must be in the list and selected
2030 :param bookmark: a bookmark that must be in the list and selected
2031 """
2031 """
2032 _ = translator or get_current_request().translate
2032 _ = translator or get_current_request().translate
2033
2033
2034 commit_id = safe_str(commit_id) if commit_id else None
2034 commit_id = safe_str(commit_id) if commit_id else None
2035 branch = safe_unicode(branch) if branch else None
2035 branch = safe_unicode(branch) if branch else None
2036 bookmark = safe_unicode(bookmark) if bookmark else None
2036 bookmark = safe_unicode(bookmark) if bookmark else None
2037
2037
2038 selected = None
2038 selected = None
2039
2039
2040 # order matters: first source that has commit_id in it will be selected
2040 # order matters: first source that has commit_id in it will be selected
2041 sources = []
2041 sources = []
2042 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
2042 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
2043 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
2043 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
2044
2044
2045 if commit_id:
2045 if commit_id:
2046 ref_commit = (h.short_id(commit_id), commit_id)
2046 ref_commit = (h.short_id(commit_id), commit_id)
2047 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
2047 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
2048
2048
2049 sources.append(
2049 sources.append(
2050 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
2050 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
2051 )
2051 )
2052
2052
2053 groups = []
2053 groups = []
2054
2054
2055 for group_key, ref_list, group_name, match in sources:
2055 for group_key, ref_list, group_name, match in sources:
2056 group_refs = []
2056 group_refs = []
2057 for ref_name, ref_id in ref_list:
2057 for ref_name, ref_id in ref_list:
2058 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
2058 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
2059 group_refs.append((ref_key, ref_name))
2059 group_refs.append((ref_key, ref_name))
2060
2060
2061 if not selected:
2061 if not selected:
2062 if set([commit_id, match]) & set([ref_id, ref_name]):
2062 if set([commit_id, match]) & set([ref_id, ref_name]):
2063 selected = ref_key
2063 selected = ref_key
2064
2064
2065 if group_refs:
2065 if group_refs:
2066 groups.append((group_refs, group_name))
2066 groups.append((group_refs, group_name))
2067
2067
2068 if not selected:
2068 if not selected:
2069 ref = commit_id or branch or bookmark
2069 ref = commit_id or branch or bookmark
2070 if ref:
2070 if ref:
2071 raise CommitDoesNotExistError(
2071 raise CommitDoesNotExistError(
2072 u'No commit refs could be found matching: {}'.format(ref))
2072 u'No commit refs could be found matching: {}'.format(ref))
2073 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
2073 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
2074 selected = u'branch:{}:{}'.format(
2074 selected = u'branch:{}:{}'.format(
2075 safe_unicode(repo.DEFAULT_BRANCH_NAME),
2075 safe_unicode(repo.DEFAULT_BRANCH_NAME),
2076 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
2076 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
2077 )
2077 )
2078 elif repo.commit_ids:
2078 elif repo.commit_ids:
2079 # make the user select in this case
2079 # make the user select in this case
2080 selected = None
2080 selected = None
2081 else:
2081 else:
2082 raise EmptyRepositoryError()
2082 raise EmptyRepositoryError()
2083 return groups, selected
2083 return groups, selected
2084
2084
2085 def get_diff(self, source_repo, source_ref_id, target_ref_id,
2085 def get_diff(self, source_repo, source_ref_id, target_ref_id,
2086 hide_whitespace_changes, diff_context):
2086 hide_whitespace_changes, diff_context):
2087
2087
2088 return self._get_diff_from_pr_or_version(
2088 return self._get_diff_from_pr_or_version(
2089 source_repo, source_ref_id, target_ref_id,
2089 source_repo, source_ref_id, target_ref_id,
2090 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
2090 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
2091
2091
2092 def _get_diff_from_pr_or_version(
2092 def _get_diff_from_pr_or_version(
2093 self, source_repo, source_ref_id, target_ref_id,
2093 self, source_repo, source_ref_id, target_ref_id,
2094 hide_whitespace_changes, diff_context):
2094 hide_whitespace_changes, diff_context):
2095
2095
2096 target_commit = source_repo.get_commit(
2096 target_commit = source_repo.get_commit(
2097 commit_id=safe_str(target_ref_id))
2097 commit_id=safe_str(target_ref_id))
2098 source_commit = source_repo.get_commit(
2098 source_commit = source_repo.get_commit(
2099 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
2099 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
2100 if isinstance(source_repo, Repository):
2100 if isinstance(source_repo, Repository):
2101 vcs_repo = source_repo.scm_instance()
2101 vcs_repo = source_repo.scm_instance()
2102 else:
2102 else:
2103 vcs_repo = source_repo
2103 vcs_repo = source_repo
2104
2104
2105 # TODO: johbo: In the context of an update, we cannot reach
2105 # TODO: johbo: In the context of an update, we cannot reach
2106 # the old commit anymore with our normal mechanisms. It needs
2106 # the old commit anymore with our normal mechanisms. It needs
2107 # some sort of special support in the vcs layer to avoid this
2107 # some sort of special support in the vcs layer to avoid this
2108 # workaround.
2108 # workaround.
2109 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
2109 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
2110 vcs_repo.alias == 'git'):
2110 vcs_repo.alias == 'git'):
2111 source_commit.raw_id = safe_str(source_ref_id)
2111 source_commit.raw_id = safe_str(source_ref_id)
2112
2112
2113 log.debug('calculating diff between '
2113 log.debug('calculating diff between '
2114 'source_ref:%s and target_ref:%s for repo `%s`',
2114 'source_ref:%s and target_ref:%s for repo `%s`',
2115 target_ref_id, source_ref_id,
2115 target_ref_id, source_ref_id,
2116 safe_unicode(vcs_repo.path))
2116 safe_unicode(vcs_repo.path))
2117
2117
2118 vcs_diff = vcs_repo.get_diff(
2118 vcs_diff = vcs_repo.get_diff(
2119 commit1=target_commit, commit2=source_commit,
2119 commit1=target_commit, commit2=source_commit,
2120 ignore_whitespace=hide_whitespace_changes, context=diff_context)
2120 ignore_whitespace=hide_whitespace_changes, context=diff_context)
2121 return vcs_diff
2121 return vcs_diff
2122
2122
2123 def _is_merge_enabled(self, pull_request):
2123 def _is_merge_enabled(self, pull_request):
2124 return self._get_general_setting(
2124 return self._get_general_setting(
2125 pull_request, 'rhodecode_pr_merge_enabled')
2125 pull_request, 'rhodecode_pr_merge_enabled')
2126
2126
2127 def _use_rebase_for_merging(self, pull_request):
2127 def _use_rebase_for_merging(self, pull_request):
2128 repo_type = pull_request.target_repo.repo_type
2128 repo_type = pull_request.target_repo.repo_type
2129 if repo_type == 'hg':
2129 if repo_type == 'hg':
2130 return self._get_general_setting(
2130 return self._get_general_setting(
2131 pull_request, 'rhodecode_hg_use_rebase_for_merging')
2131 pull_request, 'rhodecode_hg_use_rebase_for_merging')
2132 elif repo_type == 'git':
2132 elif repo_type == 'git':
2133 return self._get_general_setting(
2133 return self._get_general_setting(
2134 pull_request, 'rhodecode_git_use_rebase_for_merging')
2134 pull_request, 'rhodecode_git_use_rebase_for_merging')
2135
2135
2136 return False
2136 return False
2137
2137
2138 def _user_name_for_merging(self, pull_request, user):
2138 def _user_name_for_merging(self, pull_request, user):
2139 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2139 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2140 if env_user_name_attr and hasattr(user, env_user_name_attr):
2140 if env_user_name_attr and hasattr(user, env_user_name_attr):
2141 user_name_attr = env_user_name_attr
2141 user_name_attr = env_user_name_attr
2142 else:
2142 else:
2143 user_name_attr = 'short_contact'
2143 user_name_attr = 'short_contact'
2144
2144
2145 user_name = getattr(user, user_name_attr)
2145 user_name = getattr(user, user_name_attr)
2146 return user_name
2146 return user_name
2147
2147
2148 def _close_branch_before_merging(self, pull_request):
2148 def _close_branch_before_merging(self, pull_request):
2149 repo_type = pull_request.target_repo.repo_type
2149 repo_type = pull_request.target_repo.repo_type
2150 if repo_type == 'hg':
2150 if repo_type == 'hg':
2151 return self._get_general_setting(
2151 return self._get_general_setting(
2152 pull_request, 'rhodecode_hg_close_branch_before_merging')
2152 pull_request, 'rhodecode_hg_close_branch_before_merging')
2153 elif repo_type == 'git':
2153 elif repo_type == 'git':
2154 return self._get_general_setting(
2154 return self._get_general_setting(
2155 pull_request, 'rhodecode_git_close_branch_before_merging')
2155 pull_request, 'rhodecode_git_close_branch_before_merging')
2156
2156
2157 return False
2157 return False
2158
2158
2159 def _get_general_setting(self, pull_request, settings_key, default=False):
2159 def _get_general_setting(self, pull_request, settings_key, default=False):
2160 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2160 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2161 settings = settings_model.get_general_settings()
2161 settings = settings_model.get_general_settings()
2162 return settings.get(settings_key, default)
2162 return settings.get(settings_key, default)
2163
2163
2164 def _log_audit_action(self, action, action_data, user, pull_request):
2164 def _log_audit_action(self, action, action_data, user, pull_request):
2165 audit_logger.store(
2165 audit_logger.store(
2166 action=action,
2166 action=action,
2167 action_data=action_data,
2167 action_data=action_data,
2168 user=user,
2168 user=user,
2169 repo=pull_request.target_repo)
2169 repo=pull_request.target_repo)
2170
2170
2171 def get_reviewer_functions(self):
2171 def get_reviewer_functions(self):
2172 """
2172 """
2173 Fetches functions for validation and fetching default reviewers.
2173 Fetches functions for validation and fetching default reviewers.
2174 If available we use the EE package, else we fallback to CE
2174 If available we use the EE package, else we fallback to CE
2175 package functions
2175 package functions
2176 """
2176 """
2177 try:
2177 try:
2178 from rc_reviewers.utils import get_default_reviewers_data
2178 from rc_reviewers.utils import get_default_reviewers_data
2179 from rc_reviewers.utils import validate_default_reviewers
2179 from rc_reviewers.utils import validate_default_reviewers
2180 from rc_reviewers.utils import validate_observers
2180 from rc_reviewers.utils import validate_observers
2181 except ImportError:
2181 except ImportError:
2182 from rhodecode.apps.repository.utils import get_default_reviewers_data
2182 from rhodecode.apps.repository.utils import get_default_reviewers_data
2183 from rhodecode.apps.repository.utils import validate_default_reviewers
2183 from rhodecode.apps.repository.utils import validate_default_reviewers
2184 from rhodecode.apps.repository.utils import validate_observers
2184 from rhodecode.apps.repository.utils import validate_observers
2185
2185
2186 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2186 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2187
2187
2188
2188
2189 class MergeCheck(object):
2189 class MergeCheck(object):
2190 """
2190 """
2191 Perform Merge Checks and returns a check object which stores information
2191 Perform Merge Checks and returns a check object which stores information
2192 about merge errors, and merge conditions
2192 about merge errors, and merge conditions
2193 """
2193 """
2194 TODO_CHECK = 'todo'
2194 TODO_CHECK = 'todo'
2195 PERM_CHECK = 'perm'
2195 PERM_CHECK = 'perm'
2196 REVIEW_CHECK = 'review'
2196 REVIEW_CHECK = 'review'
2197 MERGE_CHECK = 'merge'
2197 MERGE_CHECK = 'merge'
2198 WIP_CHECK = 'wip'
2198 WIP_CHECK = 'wip'
2199
2199
2200 def __init__(self):
2200 def __init__(self):
2201 self.review_status = None
2201 self.review_status = None
2202 self.merge_possible = None
2202 self.merge_possible = None
2203 self.merge_msg = ''
2203 self.merge_msg = ''
2204 self.merge_response = None
2204 self.merge_response = None
2205 self.failed = None
2205 self.failed = None
2206 self.errors = []
2206 self.errors = []
2207 self.error_details = OrderedDict()
2207 self.error_details = OrderedDict()
2208 self.source_commit = AttributeDict()
2208 self.source_commit = AttributeDict()
2209 self.target_commit = AttributeDict()
2209 self.target_commit = AttributeDict()
2210 self.reviewers_count = 0
2210 self.reviewers_count = 0
2211 self.observers_count = 0
2211 self.observers_count = 0
2212
2212
2213 def __repr__(self):
2213 def __repr__(self):
2214 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2214 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2215 self.merge_possible, self.failed, self.errors)
2215 self.merge_possible, self.failed, self.errors)
2216
2216
2217 def push_error(self, error_type, message, error_key, details):
2217 def push_error(self, error_type, message, error_key, details):
2218 self.failed = True
2218 self.failed = True
2219 self.errors.append([error_type, message])
2219 self.errors.append([error_type, message])
2220 self.error_details[error_key] = dict(
2220 self.error_details[error_key] = dict(
2221 details=details,
2221 details=details,
2222 error_type=error_type,
2222 error_type=error_type,
2223 message=message
2223 message=message
2224 )
2224 )
2225
2225
2226 @classmethod
2226 @classmethod
2227 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2227 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2228 force_shadow_repo_refresh=False):
2228 force_shadow_repo_refresh=False):
2229 _ = translator
2229 _ = translator
2230 merge_check = cls()
2230 merge_check = cls()
2231
2231
2232 # title has WIP:
2232 # title has WIP:
2233 if pull_request.work_in_progress:
2233 if pull_request.work_in_progress:
2234 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2234 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2235
2235
2236 msg = _('WIP marker in title prevents from accidental merge.')
2236 msg = _('WIP marker in title prevents from accidental merge.')
2237 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2237 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2238 if fail_early:
2238 if fail_early:
2239 return merge_check
2239 return merge_check
2240
2240
2241 # permissions to merge
2241 # permissions to merge
2242 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2242 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2243 if not user_allowed_to_merge:
2243 if not user_allowed_to_merge:
2244 log.debug("MergeCheck: cannot merge, approval is pending.")
2244 log.debug("MergeCheck: cannot merge, approval is pending.")
2245
2245
2246 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2246 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2247 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2247 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2248 if fail_early:
2248 if fail_early:
2249 return merge_check
2249 return merge_check
2250
2250
2251 # permission to merge into the target branch
2251 # permission to merge into the target branch
2252 target_commit_id = pull_request.target_ref_parts.commit_id
2252 target_commit_id = pull_request.target_ref_parts.commit_id
2253 if pull_request.target_ref_parts.type == 'branch':
2253 if pull_request.target_ref_parts.type == 'branch':
2254 branch_name = pull_request.target_ref_parts.name
2254 branch_name = pull_request.target_ref_parts.name
2255 else:
2255 else:
2256 # for mercurial we can always figure out the branch from the commit
2256 # for mercurial we can always figure out the branch from the commit
2257 # in case of bookmark
2257 # in case of bookmark
2258 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2258 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2259 branch_name = target_commit.branch
2259 branch_name = target_commit.branch
2260
2260
2261 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2261 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2262 pull_request.target_repo.repo_name, branch_name)
2262 pull_request.target_repo.repo_name, branch_name)
2263 if branch_perm and branch_perm == 'branch.none':
2263 if branch_perm and branch_perm == 'branch.none':
2264 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2264 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2265 branch_name, rule)
2265 branch_name, rule)
2266 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2266 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2267 if fail_early:
2267 if fail_early:
2268 return merge_check
2268 return merge_check
2269
2269
2270 # review status, must be always present
2270 # review status, must be always present
2271 review_status = pull_request.calculated_review_status()
2271 review_status = pull_request.calculated_review_status()
2272 merge_check.review_status = review_status
2272 merge_check.review_status = review_status
2273 merge_check.reviewers_count = pull_request.reviewers_count
2273 merge_check.reviewers_count = pull_request.reviewers_count
2274 merge_check.observers_count = pull_request.observers_count
2274 merge_check.observers_count = pull_request.observers_count
2275
2275
2276 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2276 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2277 if not status_approved and merge_check.reviewers_count:
2277 if not status_approved and merge_check.reviewers_count:
2278 log.debug("MergeCheck: cannot merge, approval is pending.")
2278 log.debug("MergeCheck: cannot merge, approval is pending.")
2279 msg = _('Pull request reviewer approval is pending.')
2279 msg = _('Pull request reviewer approval is pending.')
2280
2280
2281 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2281 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2282
2282
2283 if fail_early:
2283 if fail_early:
2284 return merge_check
2284 return merge_check
2285
2285
2286 # left over TODOs
2286 # left over TODOs
2287 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2287 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2288 if todos:
2288 if todos:
2289 log.debug("MergeCheck: cannot merge, {} "
2289 log.debug("MergeCheck: cannot merge, {} "
2290 "unresolved TODOs left.".format(len(todos)))
2290 "unresolved TODOs left.".format(len(todos)))
2291
2291
2292 if len(todos) == 1:
2292 if len(todos) == 1:
2293 msg = _('Cannot merge, {} TODO still not resolved.').format(
2293 msg = _('Cannot merge, {} TODO still not resolved.').format(
2294 len(todos))
2294 len(todos))
2295 else:
2295 else:
2296 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2296 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2297 len(todos))
2297 len(todos))
2298
2298
2299 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2299 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2300
2300
2301 if fail_early:
2301 if fail_early:
2302 return merge_check
2302 return merge_check
2303
2303
2304 # merge possible, here is the filesystem simulation + shadow repo
2304 # merge possible, here is the filesystem simulation + shadow repo
2305 merge_response, merge_status, msg = PullRequestModel().merge_status(
2305 merge_response, merge_status, msg = PullRequestModel().merge_status(
2306 pull_request, translator=translator,
2306 pull_request, translator=translator,
2307 force_shadow_repo_refresh=force_shadow_repo_refresh)
2307 force_shadow_repo_refresh=force_shadow_repo_refresh)
2308
2308
2309 merge_check.merge_possible = merge_status
2309 merge_check.merge_possible = merge_status
2310 merge_check.merge_msg = msg
2310 merge_check.merge_msg = msg
2311 merge_check.merge_response = merge_response
2311 merge_check.merge_response = merge_response
2312
2312
2313 source_ref_id = pull_request.source_ref_parts.commit_id
2313 source_ref_id = pull_request.source_ref_parts.commit_id
2314 target_ref_id = pull_request.target_ref_parts.commit_id
2314 target_ref_id = pull_request.target_ref_parts.commit_id
2315
2315
2316 try:
2316 try:
2317 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2317 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2318 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2318 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2319 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2319 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2320 merge_check.source_commit.current_raw_id = source_commit.raw_id
2320 merge_check.source_commit.current_raw_id = source_commit.raw_id
2321 merge_check.source_commit.previous_raw_id = source_ref_id
2321 merge_check.source_commit.previous_raw_id = source_ref_id
2322
2322
2323 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2323 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2324 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2324 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2325 merge_check.target_commit.current_raw_id = target_commit.raw_id
2325 merge_check.target_commit.current_raw_id = target_commit.raw_id
2326 merge_check.target_commit.previous_raw_id = target_ref_id
2326 merge_check.target_commit.previous_raw_id = target_ref_id
2327 except (SourceRefMissing, TargetRefMissing):
2327 except (SourceRefMissing, TargetRefMissing):
2328 pass
2328 pass
2329
2329
2330 if not merge_status:
2330 if not merge_status:
2331 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2331 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2332 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2332 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2333
2333
2334 if fail_early:
2334 if fail_early:
2335 return merge_check
2335 return merge_check
2336
2336
2337 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2337 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2338 return merge_check
2338 return merge_check
2339
2339
2340 @classmethod
2340 @classmethod
2341 def get_merge_conditions(cls, pull_request, translator):
2341 def get_merge_conditions(cls, pull_request, translator):
2342 _ = translator
2342 _ = translator
2343 merge_details = {}
2343 merge_details = {}
2344
2344
2345 model = PullRequestModel()
2345 model = PullRequestModel()
2346 use_rebase = model._use_rebase_for_merging(pull_request)
2346 use_rebase = model._use_rebase_for_merging(pull_request)
2347
2347
2348 if use_rebase:
2348 if use_rebase:
2349 merge_details['merge_strategy'] = dict(
2349 merge_details['merge_strategy'] = dict(
2350 details={},
2350 details={},
2351 message=_('Merge strategy: rebase')
2351 message=_('Merge strategy: rebase')
2352 )
2352 )
2353 else:
2353 else:
2354 merge_details['merge_strategy'] = dict(
2354 merge_details['merge_strategy'] = dict(
2355 details={},
2355 details={},
2356 message=_('Merge strategy: explicit merge commit')
2356 message=_('Merge strategy: explicit merge commit')
2357 )
2357 )
2358
2358
2359 close_branch = model._close_branch_before_merging(pull_request)
2359 close_branch = model._close_branch_before_merging(pull_request)
2360 if close_branch:
2360 if close_branch:
2361 repo_type = pull_request.target_repo.repo_type
2361 repo_type = pull_request.target_repo.repo_type
2362 close_msg = ''
2362 close_msg = ''
2363 if repo_type == 'hg':
2363 if repo_type == 'hg':
2364 close_msg = _('Source branch will be closed before the merge.')
2364 close_msg = _('Source branch will be closed before the merge.')
2365 elif repo_type == 'git':
2365 elif repo_type == 'git':
2366 close_msg = _('Source branch will be deleted after the merge.')
2366 close_msg = _('Source branch will be deleted after the merge.')
2367
2367
2368 merge_details['close_branch'] = dict(
2368 merge_details['close_branch'] = dict(
2369 details={},
2369 details={},
2370 message=close_msg
2370 message=close_msg
2371 )
2371 )
2372
2372
2373 return merge_details
2373 return merge_details
2374
2374
2375
2375
2376 ChangeTuple = collections.namedtuple(
2376 ChangeTuple = collections.namedtuple(
2377 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2377 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2378
2378
2379 FileChangeTuple = collections.namedtuple(
2379 FileChangeTuple = collections.namedtuple(
2380 'FileChangeTuple', ['added', 'modified', 'removed'])
2380 'FileChangeTuple', ['added', 'modified', 'removed'])
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now