##// END OF EJS Templates
reviewers: added observers as another way to define reviewers....
marcink -
r4500:bfede169 stable
parent child Browse files
Show More

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

@@ -0,0 +1,68 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.RepoReviewRuleUser.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31 new_column = Column('role', Unicode(255), nullable=True)
32 batch_op.add_column(new_column)
33
34 _fill_rule_user_role(op, meta.Session)
35
36 table = db.RepoReviewRuleUserGroup.__table__
37 with op.batch_alter_table(table.name) as batch_op:
38 new_column = Column('role', Unicode(255), nullable=True)
39 batch_op.add_column(new_column)
40
41 _fill_rule_user_group_role(op, meta.Session)
42
43
44 def downgrade(migrate_engine):
45 meta = MetaData()
46 meta.bind = migrate_engine
47
48
49 def fixups(models, _SESSION):
50 pass
51
52
53 def _fill_rule_user_role(op, session):
54 params = {'role': 'reviewer'}
55 query = text(
56 'UPDATE repo_review_rules_users SET role = :role'
57 ).bindparams(**params)
58 op.execute(query)
59 session().commit()
60
61
62 def _fill_rule_user_group_role(op, session):
63 params = {'role': 'reviewer'}
64 query = text(
65 'UPDATE repo_review_rules_users_groups SET role = :role'
66 ).bindparams(**params)
67 op.execute(query)
68 session().commit()
@@ -0,0 +1,35 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 def html(info):
23 """
24 Custom string as html content_type renderer for pyramid
25 """
26 def _render(value, system):
27 request = system.get('request')
28 if request is not None:
29 response = request.response
30 ct = response.content_type
31 if ct == response.default_content_type:
32 response.content_type = 'text/html'
33 return value
34
35 return _render
@@ -1,60 +1,60 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 import os
21 import os
22 from collections import OrderedDict
22 from collections import OrderedDict
23
23
24 import sys
24 import sys
25 import platform
25 import platform
26
26
27 VERSION = tuple(open(os.path.join(
27 VERSION = tuple(open(os.path.join(
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
29
29
30 BACKENDS = OrderedDict()
30 BACKENDS = OrderedDict()
31
31
32 BACKENDS['hg'] = 'Mercurial repository'
32 BACKENDS['hg'] = 'Mercurial repository'
33 BACKENDS['git'] = 'Git repository'
33 BACKENDS['git'] = 'Git repository'
34 BACKENDS['svn'] = 'Subversion repository'
34 BACKENDS['svn'] = 'Subversion repository'
35
35
36
36
37 CELERY_ENABLED = False
37 CELERY_ENABLED = False
38 CELERY_EAGER = False
38 CELERY_EAGER = False
39
39
40 # link to config for pyramid
40 # link to config for pyramid
41 CONFIG = {}
41 CONFIG = {}
42
42
43 # Populated with the settings dictionary from application init in
43 # Populated with the settings dictionary from application init in
44 # rhodecode.conf.environment.load_pyramid_environment
44 # rhodecode.conf.environment.load_pyramid_environment
45 PYRAMID_SETTINGS = {}
45 PYRAMID_SETTINGS = {}
46
46
47 # Linked module for extensions
47 # Linked module for extensions
48 EXTENSIONS = {}
48 EXTENSIONS = {}
49
49
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 109 # defines current db version for migrations
51 __dbversion__ = 110 # defines current db version for migrations
52 __platform__ = platform.system()
52 __platform__ = platform.system()
53 __license__ = 'AGPLv3, and Commercial License'
53 __license__ = 'AGPLv3, and Commercial License'
54 __author__ = 'RhodeCode GmbH'
54 __author__ = 'RhodeCode GmbH'
55 __url__ = 'https://code.rhodecode.com'
55 __url__ = 'https://code.rhodecode.com'
56
56
57 is_windows = __platform__ in ['Windows']
57 is_windows = __platform__ in ['Windows']
58 is_unix = not is_windows
58 is_unix = not is_windows
59 is_test = False
59 is_test = False
60 disable_error_handler = False
60 disable_error_handler = False
@@ -1,1018 +1,1017 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
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 import logging
22 import logging
23
23
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.changeset_status import ChangesetStatusModel
32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.validation_schema import Invalid
37 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 @jsonrpc_method()
43 @jsonrpc_method()
44 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
44 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
45 merge_state=Optional(False)):
45 merge_state=Optional(False)):
46 """
46 """
47 Get a pull request based on the given ID.
47 Get a pull request based on the given ID.
48
48
49 :param apiuser: This is filled automatically from the |authtoken|.
49 :param apiuser: This is filled automatically from the |authtoken|.
50 :type apiuser: AuthUser
50 :type apiuser: AuthUser
51 :param repoid: Optional, repository name or repository ID from where
51 :param repoid: Optional, repository name or repository ID from where
52 the pull request was opened.
52 the pull request was opened.
53 :type repoid: str or int
53 :type repoid: str or int
54 :param pullrequestid: ID of the requested pull request.
54 :param pullrequestid: ID of the requested pull request.
55 :type pullrequestid: int
55 :type pullrequestid: int
56 :param merge_state: Optional calculate merge state for each repository.
56 :param merge_state: Optional calculate merge state for each repository.
57 This could result in longer time to fetch the data
57 This could result in longer time to fetch the data
58 :type merge_state: bool
58 :type merge_state: bool
59
59
60 Example output:
60 Example output:
61
61
62 .. code-block:: bash
62 .. code-block:: bash
63
63
64 "id": <id_given_in_input>,
64 "id": <id_given_in_input>,
65 "result":
65 "result":
66 {
66 {
67 "pull_request_id": "<pull_request_id>",
67 "pull_request_id": "<pull_request_id>",
68 "url": "<url>",
68 "url": "<url>",
69 "title": "<title>",
69 "title": "<title>",
70 "description": "<description>",
70 "description": "<description>",
71 "status" : "<status>",
71 "status" : "<status>",
72 "created_on": "<date_time_created>",
72 "created_on": "<date_time_created>",
73 "updated_on": "<date_time_updated>",
73 "updated_on": "<date_time_updated>",
74 "versions": "<number_or_versions_of_pr>",
74 "versions": "<number_or_versions_of_pr>",
75 "commit_ids": [
75 "commit_ids": [
76 ...
76 ...
77 "<commit_id>",
77 "<commit_id>",
78 "<commit_id>",
78 "<commit_id>",
79 ...
79 ...
80 ],
80 ],
81 "review_status": "<review_status>",
81 "review_status": "<review_status>",
82 "mergeable": {
82 "mergeable": {
83 "status": "<bool>",
83 "status": "<bool>",
84 "message": "<message>",
84 "message": "<message>",
85 },
85 },
86 "source": {
86 "source": {
87 "clone_url": "<clone_url>",
87 "clone_url": "<clone_url>",
88 "repository": "<repository_name>",
88 "repository": "<repository_name>",
89 "reference":
89 "reference":
90 {
90 {
91 "name": "<name>",
91 "name": "<name>",
92 "type": "<type>",
92 "type": "<type>",
93 "commit_id": "<commit_id>",
93 "commit_id": "<commit_id>",
94 }
94 }
95 },
95 },
96 "target": {
96 "target": {
97 "clone_url": "<clone_url>",
97 "clone_url": "<clone_url>",
98 "repository": "<repository_name>",
98 "repository": "<repository_name>",
99 "reference":
99 "reference":
100 {
100 {
101 "name": "<name>",
101 "name": "<name>",
102 "type": "<type>",
102 "type": "<type>",
103 "commit_id": "<commit_id>",
103 "commit_id": "<commit_id>",
104 }
104 }
105 },
105 },
106 "merge": {
106 "merge": {
107 "clone_url": "<clone_url>",
107 "clone_url": "<clone_url>",
108 "reference":
108 "reference":
109 {
109 {
110 "name": "<name>",
110 "name": "<name>",
111 "type": "<type>",
111 "type": "<type>",
112 "commit_id": "<commit_id>",
112 "commit_id": "<commit_id>",
113 }
113 }
114 },
114 },
115 "author": <user_obj>,
115 "author": <user_obj>,
116 "reviewers": [
116 "reviewers": [
117 ...
117 ...
118 {
118 {
119 "user": "<user_obj>",
119 "user": "<user_obj>",
120 "review_status": "<review_status>",
120 "review_status": "<review_status>",
121 }
121 }
122 ...
122 ...
123 ]
123 ]
124 },
124 },
125 "error": null
125 "error": null
126 """
126 """
127
127
128 pull_request = get_pull_request_or_error(pullrequestid)
128 pull_request = get_pull_request_or_error(pullrequestid)
129 if Optional.extract(repoid):
129 if Optional.extract(repoid):
130 repo = get_repo_or_error(repoid)
130 repo = get_repo_or_error(repoid)
131 else:
131 else:
132 repo = pull_request.target_repo
132 repo = pull_request.target_repo
133
133
134 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
134 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
135 raise JSONRPCError('repository `%s` or pull request `%s` '
135 raise JSONRPCError('repository `%s` or pull request `%s` '
136 'does not exist' % (repoid, pullrequestid))
136 'does not exist' % (repoid, pullrequestid))
137
137
138 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
138 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
139 # otherwise we can lock the repo on calculation of merge state while update/merge
139 # otherwise we can lock the repo on calculation of merge state while update/merge
140 # is happening.
140 # is happening.
141 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
141 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
142 merge_state = Optional.extract(merge_state, binary=True) and pr_created
142 merge_state = Optional.extract(merge_state, binary=True) and pr_created
143 data = pull_request.get_api_data(with_merge_state=merge_state)
143 data = pull_request.get_api_data(with_merge_state=merge_state)
144 return data
144 return data
145
145
146
146
147 @jsonrpc_method()
147 @jsonrpc_method()
148 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
148 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
149 merge_state=Optional(False)):
149 merge_state=Optional(False)):
150 """
150 """
151 Get all pull requests from the repository specified in `repoid`.
151 Get all pull requests from the repository specified in `repoid`.
152
152
153 :param apiuser: This is filled automatically from the |authtoken|.
153 :param apiuser: This is filled automatically from the |authtoken|.
154 :type apiuser: AuthUser
154 :type apiuser: AuthUser
155 :param repoid: Optional repository name or repository ID.
155 :param repoid: Optional repository name or repository ID.
156 :type repoid: str or int
156 :type repoid: str or int
157 :param status: Only return pull requests with the specified status.
157 :param status: Only return pull requests with the specified status.
158 Valid options are.
158 Valid options are.
159 * ``new`` (default)
159 * ``new`` (default)
160 * ``open``
160 * ``open``
161 * ``closed``
161 * ``closed``
162 :type status: str
162 :type status: str
163 :param merge_state: Optional calculate merge state for each repository.
163 :param merge_state: Optional calculate merge state for each repository.
164 This could result in longer time to fetch the data
164 This could result in longer time to fetch the data
165 :type merge_state: bool
165 :type merge_state: bool
166
166
167 Example output:
167 Example output:
168
168
169 .. code-block:: bash
169 .. code-block:: bash
170
170
171 "id": <id_given_in_input>,
171 "id": <id_given_in_input>,
172 "result":
172 "result":
173 [
173 [
174 ...
174 ...
175 {
175 {
176 "pull_request_id": "<pull_request_id>",
176 "pull_request_id": "<pull_request_id>",
177 "url": "<url>",
177 "url": "<url>",
178 "title" : "<title>",
178 "title" : "<title>",
179 "description": "<description>",
179 "description": "<description>",
180 "status": "<status>",
180 "status": "<status>",
181 "created_on": "<date_time_created>",
181 "created_on": "<date_time_created>",
182 "updated_on": "<date_time_updated>",
182 "updated_on": "<date_time_updated>",
183 "commit_ids": [
183 "commit_ids": [
184 ...
184 ...
185 "<commit_id>",
185 "<commit_id>",
186 "<commit_id>",
186 "<commit_id>",
187 ...
187 ...
188 ],
188 ],
189 "review_status": "<review_status>",
189 "review_status": "<review_status>",
190 "mergeable": {
190 "mergeable": {
191 "status": "<bool>",
191 "status": "<bool>",
192 "message: "<message>",
192 "message: "<message>",
193 },
193 },
194 "source": {
194 "source": {
195 "clone_url": "<clone_url>",
195 "clone_url": "<clone_url>",
196 "reference":
196 "reference":
197 {
197 {
198 "name": "<name>",
198 "name": "<name>",
199 "type": "<type>",
199 "type": "<type>",
200 "commit_id": "<commit_id>",
200 "commit_id": "<commit_id>",
201 }
201 }
202 },
202 },
203 "target": {
203 "target": {
204 "clone_url": "<clone_url>",
204 "clone_url": "<clone_url>",
205 "reference":
205 "reference":
206 {
206 {
207 "name": "<name>",
207 "name": "<name>",
208 "type": "<type>",
208 "type": "<type>",
209 "commit_id": "<commit_id>",
209 "commit_id": "<commit_id>",
210 }
210 }
211 },
211 },
212 "merge": {
212 "merge": {
213 "clone_url": "<clone_url>",
213 "clone_url": "<clone_url>",
214 "reference":
214 "reference":
215 {
215 {
216 "name": "<name>",
216 "name": "<name>",
217 "type": "<type>",
217 "type": "<type>",
218 "commit_id": "<commit_id>",
218 "commit_id": "<commit_id>",
219 }
219 }
220 },
220 },
221 "author": <user_obj>,
221 "author": <user_obj>,
222 "reviewers": [
222 "reviewers": [
223 ...
223 ...
224 {
224 {
225 "user": "<user_obj>",
225 "user": "<user_obj>",
226 "review_status": "<review_status>",
226 "review_status": "<review_status>",
227 }
227 }
228 ...
228 ...
229 ]
229 ]
230 }
230 }
231 ...
231 ...
232 ],
232 ],
233 "error": null
233 "error": null
234
234
235 """
235 """
236 repo = get_repo_or_error(repoid)
236 repo = get_repo_or_error(repoid)
237 if not has_superadmin_permission(apiuser):
237 if not has_superadmin_permission(apiuser):
238 _perms = (
238 _perms = (
239 'repository.admin', 'repository.write', 'repository.read',)
239 'repository.admin', 'repository.write', 'repository.read',)
240 validate_repo_permissions(apiuser, repoid, repo, _perms)
240 validate_repo_permissions(apiuser, repoid, repo, _perms)
241
241
242 status = Optional.extract(status)
242 status = Optional.extract(status)
243 merge_state = Optional.extract(merge_state, binary=True)
243 merge_state = Optional.extract(merge_state, binary=True)
244 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
244 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
245 order_by='id', order_dir='desc')
245 order_by='id', order_dir='desc')
246 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
246 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
247 return data
247 return data
248
248
249
249
250 @jsonrpc_method()
250 @jsonrpc_method()
251 def merge_pull_request(
251 def merge_pull_request(
252 request, apiuser, pullrequestid, repoid=Optional(None),
252 request, apiuser, pullrequestid, repoid=Optional(None),
253 userid=Optional(OAttr('apiuser'))):
253 userid=Optional(OAttr('apiuser'))):
254 """
254 """
255 Merge the pull request specified by `pullrequestid` into its target
255 Merge the pull request specified by `pullrequestid` into its target
256 repository.
256 repository.
257
257
258 :param apiuser: This is filled automatically from the |authtoken|.
258 :param apiuser: This is filled automatically from the |authtoken|.
259 :type apiuser: AuthUser
259 :type apiuser: AuthUser
260 :param repoid: Optional, repository name or repository ID of the
260 :param repoid: Optional, repository name or repository ID of the
261 target repository to which the |pr| is to be merged.
261 target repository to which the |pr| is to be merged.
262 :type repoid: str or int
262 :type repoid: str or int
263 :param pullrequestid: ID of the pull request which shall be merged.
263 :param pullrequestid: ID of the pull request which shall be merged.
264 :type pullrequestid: int
264 :type pullrequestid: int
265 :param userid: Merge the pull request as this user.
265 :param userid: Merge the pull request as this user.
266 :type userid: Optional(str or int)
266 :type userid: Optional(str or int)
267
267
268 Example output:
268 Example output:
269
269
270 .. code-block:: bash
270 .. code-block:: bash
271
271
272 "id": <id_given_in_input>,
272 "id": <id_given_in_input>,
273 "result": {
273 "result": {
274 "executed": "<bool>",
274 "executed": "<bool>",
275 "failure_reason": "<int>",
275 "failure_reason": "<int>",
276 "merge_status_message": "<str>",
276 "merge_status_message": "<str>",
277 "merge_commit_id": "<merge_commit_id>",
277 "merge_commit_id": "<merge_commit_id>",
278 "possible": "<bool>",
278 "possible": "<bool>",
279 "merge_ref": {
279 "merge_ref": {
280 "commit_id": "<commit_id>",
280 "commit_id": "<commit_id>",
281 "type": "<type>",
281 "type": "<type>",
282 "name": "<name>"
282 "name": "<name>"
283 }
283 }
284 },
284 },
285 "error": null
285 "error": null
286 """
286 """
287 pull_request = get_pull_request_or_error(pullrequestid)
287 pull_request = get_pull_request_or_error(pullrequestid)
288 if Optional.extract(repoid):
288 if Optional.extract(repoid):
289 repo = get_repo_or_error(repoid)
289 repo = get_repo_or_error(repoid)
290 else:
290 else:
291 repo = pull_request.target_repo
291 repo = pull_request.target_repo
292 auth_user = apiuser
292 auth_user = apiuser
293
293
294 if not isinstance(userid, Optional):
294 if not isinstance(userid, Optional):
295 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
295 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
296 user=apiuser, repo_name=repo.repo_name)
296 user=apiuser, repo_name=repo.repo_name)
297 if has_superadmin_permission(apiuser) or is_repo_admin:
297 if has_superadmin_permission(apiuser) or is_repo_admin:
298 apiuser = get_user_or_error(userid)
298 apiuser = get_user_or_error(userid)
299 auth_user = apiuser.AuthUser()
299 auth_user = apiuser.AuthUser()
300 else:
300 else:
301 raise JSONRPCError('userid is not the same as your user')
301 raise JSONRPCError('userid is not the same as your user')
302
302
303 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
303 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 raise JSONRPCError(
304 raise JSONRPCError(
305 'Operation forbidden because pull request is in state {}, '
305 'Operation forbidden because pull request is in state {}, '
306 'only state {} is allowed.'.format(
306 'only state {} is allowed.'.format(
307 pull_request.pull_request_state, PullRequest.STATE_CREATED))
307 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308
308
309 with pull_request.set_state(PullRequest.STATE_UPDATING):
309 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 check = MergeCheck.validate(pull_request, auth_user=auth_user,
310 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 translator=request.translate)
311 translator=request.translate)
312 merge_possible = not check.failed
312 merge_possible = not check.failed
313
313
314 if not merge_possible:
314 if not merge_possible:
315 error_messages = []
315 error_messages = []
316 for err_type, error_msg in check.errors:
316 for err_type, error_msg in check.errors:
317 error_msg = request.translate(error_msg)
317 error_msg = request.translate(error_msg)
318 error_messages.append(error_msg)
318 error_messages.append(error_msg)
319
319
320 reasons = ','.join(error_messages)
320 reasons = ','.join(error_messages)
321 raise JSONRPCError(
321 raise JSONRPCError(
322 'merge not possible for following reasons: {}'.format(reasons))
322 'merge not possible for following reasons: {}'.format(reasons))
323
323
324 target_repo = pull_request.target_repo
324 target_repo = pull_request.target_repo
325 extras = vcs_operation_context(
325 extras = vcs_operation_context(
326 request.environ, repo_name=target_repo.repo_name,
326 request.environ, repo_name=target_repo.repo_name,
327 username=auth_user.username, action='push',
327 username=auth_user.username, action='push',
328 scm=target_repo.repo_type)
328 scm=target_repo.repo_type)
329 with pull_request.set_state(PullRequest.STATE_UPDATING):
329 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 merge_response = PullRequestModel().merge_repo(
330 merge_response = PullRequestModel().merge_repo(
331 pull_request, apiuser, extras=extras)
331 pull_request, apiuser, extras=extras)
332 if merge_response.executed:
332 if merge_response.executed:
333 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
333 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334
334
335 Session().commit()
335 Session().commit()
336
336
337 # In previous versions the merge response directly contained the merge
337 # In previous versions the merge response directly contained the merge
338 # commit id. It is now contained in the merge reference object. To be
338 # commit id. It is now contained in the merge reference object. To be
339 # backwards compatible we have to extract it again.
339 # backwards compatible we have to extract it again.
340 merge_response = merge_response.asdict()
340 merge_response = merge_response.asdict()
341 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
341 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342
342
343 return merge_response
343 return merge_response
344
344
345
345
346 @jsonrpc_method()
346 @jsonrpc_method()
347 def get_pull_request_comments(
347 def get_pull_request_comments(
348 request, apiuser, pullrequestid, repoid=Optional(None)):
348 request, apiuser, pullrequestid, repoid=Optional(None)):
349 """
349 """
350 Get all comments of pull request specified with the `pullrequestid`
350 Get all comments of pull request specified with the `pullrequestid`
351
351
352 :param apiuser: This is filled automatically from the |authtoken|.
352 :param apiuser: This is filled automatically from the |authtoken|.
353 :type apiuser: AuthUser
353 :type apiuser: AuthUser
354 :param repoid: Optional repository name or repository ID.
354 :param repoid: Optional repository name or repository ID.
355 :type repoid: str or int
355 :type repoid: str or int
356 :param pullrequestid: The pull request ID.
356 :param pullrequestid: The pull request ID.
357 :type pullrequestid: int
357 :type pullrequestid: int
358
358
359 Example output:
359 Example output:
360
360
361 .. code-block:: bash
361 .. code-block:: bash
362
362
363 id : <id_given_in_input>
363 id : <id_given_in_input>
364 result : [
364 result : [
365 {
365 {
366 "comment_author": {
366 "comment_author": {
367 "active": true,
367 "active": true,
368 "full_name_or_username": "Tom Gore",
368 "full_name_or_username": "Tom Gore",
369 "username": "admin"
369 "username": "admin"
370 },
370 },
371 "comment_created_on": "2017-01-02T18:43:45.533",
371 "comment_created_on": "2017-01-02T18:43:45.533",
372 "comment_f_path": null,
372 "comment_f_path": null,
373 "comment_id": 25,
373 "comment_id": 25,
374 "comment_lineno": null,
374 "comment_lineno": null,
375 "comment_status": {
375 "comment_status": {
376 "status": "under_review",
376 "status": "under_review",
377 "status_lbl": "Under Review"
377 "status_lbl": "Under Review"
378 },
378 },
379 "comment_text": "Example text",
379 "comment_text": "Example text",
380 "comment_type": null,
380 "comment_type": null,
381 "comment_last_version: 0,
381 "comment_last_version: 0,
382 "pull_request_version": null,
382 "pull_request_version": null,
383 "comment_commit_id": None,
383 "comment_commit_id": None,
384 "comment_pull_request_id": <pull_request_id>
384 "comment_pull_request_id": <pull_request_id>
385 }
385 }
386 ],
386 ],
387 error : null
387 error : null
388 """
388 """
389
389
390 pull_request = get_pull_request_or_error(pullrequestid)
390 pull_request = get_pull_request_or_error(pullrequestid)
391 if Optional.extract(repoid):
391 if Optional.extract(repoid):
392 repo = get_repo_or_error(repoid)
392 repo = get_repo_or_error(repoid)
393 else:
393 else:
394 repo = pull_request.target_repo
394 repo = pull_request.target_repo
395
395
396 if not PullRequestModel().check_user_read(
396 if not PullRequestModel().check_user_read(
397 pull_request, apiuser, api=True):
397 pull_request, apiuser, api=True):
398 raise JSONRPCError('repository `%s` or pull request `%s` '
398 raise JSONRPCError('repository `%s` or pull request `%s` '
399 'does not exist' % (repoid, pullrequestid))
399 'does not exist' % (repoid, pullrequestid))
400
400
401 (pull_request_latest,
401 (pull_request_latest,
402 pull_request_at_ver,
402 pull_request_at_ver,
403 pull_request_display_obj,
403 pull_request_display_obj,
404 at_version) = PullRequestModel().get_pr_version(
404 at_version) = PullRequestModel().get_pr_version(
405 pull_request.pull_request_id, version=None)
405 pull_request.pull_request_id, version=None)
406
406
407 versions = pull_request_display_obj.versions()
407 versions = pull_request_display_obj.versions()
408 ver_map = {
408 ver_map = {
409 ver.pull_request_version_id: cnt
409 ver.pull_request_version_id: cnt
410 for cnt, ver in enumerate(versions, 1)
410 for cnt, ver in enumerate(versions, 1)
411 }
411 }
412
412
413 # GENERAL COMMENTS with versions #
413 # GENERAL COMMENTS with versions #
414 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
414 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
415 q = q.order_by(ChangesetComment.comment_id.asc())
415 q = q.order_by(ChangesetComment.comment_id.asc())
416 general_comments = q.all()
416 general_comments = q.all()
417
417
418 # INLINE COMMENTS with versions #
418 # INLINE COMMENTS with versions #
419 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
419 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
420 q = q.order_by(ChangesetComment.comment_id.asc())
420 q = q.order_by(ChangesetComment.comment_id.asc())
421 inline_comments = q.all()
421 inline_comments = q.all()
422
422
423 data = []
423 data = []
424 for comment in inline_comments + general_comments:
424 for comment in inline_comments + general_comments:
425 full_data = comment.get_api_data()
425 full_data = comment.get_api_data()
426 pr_version_id = None
426 pr_version_id = None
427 if comment.pull_request_version_id:
427 if comment.pull_request_version_id:
428 pr_version_id = 'v{}'.format(
428 pr_version_id = 'v{}'.format(
429 ver_map[comment.pull_request_version_id])
429 ver_map[comment.pull_request_version_id])
430
430
431 # sanitize some entries
431 # sanitize some entries
432
432
433 full_data['pull_request_version'] = pr_version_id
433 full_data['pull_request_version'] = pr_version_id
434 full_data['comment_author'] = {
434 full_data['comment_author'] = {
435 'username': full_data['comment_author'].username,
435 'username': full_data['comment_author'].username,
436 'full_name_or_username': full_data['comment_author'].full_name_or_username,
436 'full_name_or_username': full_data['comment_author'].full_name_or_username,
437 'active': full_data['comment_author'].active,
437 'active': full_data['comment_author'].active,
438 }
438 }
439
439
440 if full_data['comment_status']:
440 if full_data['comment_status']:
441 full_data['comment_status'] = {
441 full_data['comment_status'] = {
442 'status': full_data['comment_status'][0].status,
442 'status': full_data['comment_status'][0].status,
443 'status_lbl': full_data['comment_status'][0].status_lbl,
443 'status_lbl': full_data['comment_status'][0].status_lbl,
444 }
444 }
445 else:
445 else:
446 full_data['comment_status'] = {}
446 full_data['comment_status'] = {}
447
447
448 data.append(full_data)
448 data.append(full_data)
449 return data
449 return data
450
450
451
451
452 @jsonrpc_method()
452 @jsonrpc_method()
453 def comment_pull_request(
453 def comment_pull_request(
454 request, apiuser, pullrequestid, repoid=Optional(None),
454 request, apiuser, pullrequestid, repoid=Optional(None),
455 message=Optional(None), commit_id=Optional(None), status=Optional(None),
455 message=Optional(None), commit_id=Optional(None), status=Optional(None),
456 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
456 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
457 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
457 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
458 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
458 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
459 """
459 """
460 Comment on the pull request specified with the `pullrequestid`,
460 Comment on the pull request specified with the `pullrequestid`,
461 in the |repo| specified by the `repoid`, and optionally change the
461 in the |repo| specified by the `repoid`, and optionally change the
462 review status.
462 review status.
463
463
464 :param apiuser: This is filled automatically from the |authtoken|.
464 :param apiuser: This is filled automatically from the |authtoken|.
465 :type apiuser: AuthUser
465 :type apiuser: AuthUser
466 :param repoid: Optional repository name or repository ID.
466 :param repoid: Optional repository name or repository ID.
467 :type repoid: str or int
467 :type repoid: str or int
468 :param pullrequestid: The pull request ID.
468 :param pullrequestid: The pull request ID.
469 :type pullrequestid: int
469 :type pullrequestid: int
470 :param commit_id: Specify the commit_id for which to set a comment. If
470 :param commit_id: Specify the commit_id for which to set a comment. If
471 given commit_id is different than latest in the PR status
471 given commit_id is different than latest in the PR status
472 change won't be performed.
472 change won't be performed.
473 :type commit_id: str
473 :type commit_id: str
474 :param message: The text content of the comment.
474 :param message: The text content of the comment.
475 :type message: str
475 :type message: str
476 :param status: (**Optional**) Set the approval status of the pull
476 :param status: (**Optional**) Set the approval status of the pull
477 request. One of: 'not_reviewed', 'approved', 'rejected',
477 request. One of: 'not_reviewed', 'approved', 'rejected',
478 'under_review'
478 'under_review'
479 :type status: str
479 :type status: str
480 :param comment_type: Comment type, one of: 'note', 'todo'
480 :param comment_type: Comment type, one of: 'note', 'todo'
481 :type comment_type: Optional(str), default: 'note'
481 :type comment_type: Optional(str), default: 'note'
482 :param resolves_comment_id: id of comment which this one will resolve
482 :param resolves_comment_id: id of comment which this one will resolve
483 :type resolves_comment_id: Optional(int)
483 :type resolves_comment_id: Optional(int)
484 :param extra_recipients: list of user ids or usernames to add
484 :param extra_recipients: list of user ids or usernames to add
485 notifications for this comment. Acts like a CC for notification
485 notifications for this comment. Acts like a CC for notification
486 :type extra_recipients: Optional(list)
486 :type extra_recipients: Optional(list)
487 :param userid: Comment on the pull request as this user
487 :param userid: Comment on the pull request as this user
488 :type userid: Optional(str or int)
488 :type userid: Optional(str or int)
489 :param send_email: Define if this comment should also send email notification
489 :param send_email: Define if this comment should also send email notification
490 :type send_email: Optional(bool)
490 :type send_email: Optional(bool)
491
491
492 Example output:
492 Example output:
493
493
494 .. code-block:: bash
494 .. code-block:: bash
495
495
496 id : <id_given_in_input>
496 id : <id_given_in_input>
497 result : {
497 result : {
498 "pull_request_id": "<Integer>",
498 "pull_request_id": "<Integer>",
499 "comment_id": "<Integer>",
499 "comment_id": "<Integer>",
500 "status": {"given": <given_status>,
500 "status": {"given": <given_status>,
501 "was_changed": <bool status_was_actually_changed> },
501 "was_changed": <bool status_was_actually_changed> },
502 },
502 },
503 error : null
503 error : null
504 """
504 """
505 pull_request = get_pull_request_or_error(pullrequestid)
505 pull_request = get_pull_request_or_error(pullrequestid)
506 if Optional.extract(repoid):
506 if Optional.extract(repoid):
507 repo = get_repo_or_error(repoid)
507 repo = get_repo_or_error(repoid)
508 else:
508 else:
509 repo = pull_request.target_repo
509 repo = pull_request.target_repo
510
510
511 auth_user = apiuser
511 auth_user = apiuser
512 if not isinstance(userid, Optional):
512 if not isinstance(userid, Optional):
513 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
513 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
514 user=apiuser, repo_name=repo.repo_name)
514 user=apiuser, repo_name=repo.repo_name)
515 if has_superadmin_permission(apiuser) or is_repo_admin:
515 if has_superadmin_permission(apiuser) or is_repo_admin:
516 apiuser = get_user_or_error(userid)
516 apiuser = get_user_or_error(userid)
517 auth_user = apiuser.AuthUser()
517 auth_user = apiuser.AuthUser()
518 else:
518 else:
519 raise JSONRPCError('userid is not the same as your user')
519 raise JSONRPCError('userid is not the same as your user')
520
520
521 if pull_request.is_closed():
521 if pull_request.is_closed():
522 raise JSONRPCError(
522 raise JSONRPCError(
523 'pull request `%s` comment failed, pull request is closed' % (
523 'pull request `%s` comment failed, pull request is closed' % (
524 pullrequestid,))
524 pullrequestid,))
525
525
526 if not PullRequestModel().check_user_read(
526 if not PullRequestModel().check_user_read(
527 pull_request, apiuser, api=True):
527 pull_request, apiuser, api=True):
528 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
528 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
529 message = Optional.extract(message)
529 message = Optional.extract(message)
530 status = Optional.extract(status)
530 status = Optional.extract(status)
531 commit_id = Optional.extract(commit_id)
531 commit_id = Optional.extract(commit_id)
532 comment_type = Optional.extract(comment_type)
532 comment_type = Optional.extract(comment_type)
533 resolves_comment_id = Optional.extract(resolves_comment_id)
533 resolves_comment_id = Optional.extract(resolves_comment_id)
534 extra_recipients = Optional.extract(extra_recipients)
534 extra_recipients = Optional.extract(extra_recipients)
535 send_email = Optional.extract(send_email, binary=True)
535 send_email = Optional.extract(send_email, binary=True)
536
536
537 if not message and not status:
537 if not message and not status:
538 raise JSONRPCError(
538 raise JSONRPCError(
539 'Both message and status parameters are missing. '
539 'Both message and status parameters are missing. '
540 'At least one is required.')
540 'At least one is required.')
541
541
542 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
542 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
543 status is not None):
543 status is not None):
544 raise JSONRPCError('Unknown comment status: `%s`' % status)
544 raise JSONRPCError('Unknown comment status: `%s`' % status)
545
545
546 if commit_id and commit_id not in pull_request.revisions:
546 if commit_id and commit_id not in pull_request.revisions:
547 raise JSONRPCError(
547 raise JSONRPCError(
548 'Invalid commit_id `%s` for this pull request.' % commit_id)
548 'Invalid commit_id `%s` for this pull request.' % commit_id)
549
549
550 allowed_to_change_status = PullRequestModel().check_user_change_status(
550 allowed_to_change_status = PullRequestModel().check_user_change_status(
551 pull_request, apiuser)
551 pull_request, apiuser)
552
552
553 # if commit_id is passed re-validated if user is allowed to change status
553 # if commit_id is passed re-validated if user is allowed to change status
554 # based on latest commit_id from the PR
554 # based on latest commit_id from the PR
555 if commit_id:
555 if commit_id:
556 commit_idx = pull_request.revisions.index(commit_id)
556 commit_idx = pull_request.revisions.index(commit_id)
557 if commit_idx != 0:
557 if commit_idx != 0:
558 allowed_to_change_status = False
558 allowed_to_change_status = False
559
559
560 if resolves_comment_id:
560 if resolves_comment_id:
561 comment = ChangesetComment.get(resolves_comment_id)
561 comment = ChangesetComment.get(resolves_comment_id)
562 if not comment:
562 if not comment:
563 raise JSONRPCError(
563 raise JSONRPCError(
564 'Invalid resolves_comment_id `%s` for this pull request.'
564 'Invalid resolves_comment_id `%s` for this pull request.'
565 % resolves_comment_id)
565 % resolves_comment_id)
566 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
566 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
567 raise JSONRPCError(
567 raise JSONRPCError(
568 'Comment `%s` is wrong type for setting status to resolved.'
568 'Comment `%s` is wrong type for setting status to resolved.'
569 % resolves_comment_id)
569 % resolves_comment_id)
570
570
571 text = message
571 text = message
572 status_label = ChangesetStatus.get_status_lbl(status)
572 status_label = ChangesetStatus.get_status_lbl(status)
573 if status and allowed_to_change_status:
573 if status and allowed_to_change_status:
574 st_message = ('Status change %(transition_icon)s %(status)s'
574 st_message = ('Status change %(transition_icon)s %(status)s'
575 % {'transition_icon': '>', 'status': status_label})
575 % {'transition_icon': '>', 'status': status_label})
576 text = message or st_message
576 text = message or st_message
577
577
578 rc_config = SettingsModel().get_all_settings()
578 rc_config = SettingsModel().get_all_settings()
579 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
579 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
580
580
581 status_change = status and allowed_to_change_status
581 status_change = status and allowed_to_change_status
582 comment = CommentsModel().create(
582 comment = CommentsModel().create(
583 text=text,
583 text=text,
584 repo=pull_request.target_repo.repo_id,
584 repo=pull_request.target_repo.repo_id,
585 user=apiuser.user_id,
585 user=apiuser.user_id,
586 pull_request=pull_request.pull_request_id,
586 pull_request=pull_request.pull_request_id,
587 f_path=None,
587 f_path=None,
588 line_no=None,
588 line_no=None,
589 status_change=(status_label if status_change else None),
589 status_change=(status_label if status_change else None),
590 status_change_type=(status if status_change else None),
590 status_change_type=(status if status_change else None),
591 closing_pr=False,
591 closing_pr=False,
592 renderer=renderer,
592 renderer=renderer,
593 comment_type=comment_type,
593 comment_type=comment_type,
594 resolves_comment_id=resolves_comment_id,
594 resolves_comment_id=resolves_comment_id,
595 auth_user=auth_user,
595 auth_user=auth_user,
596 extra_recipients=extra_recipients,
596 extra_recipients=extra_recipients,
597 send_email=send_email
597 send_email=send_email
598 )
598 )
599
599
600 if allowed_to_change_status and status:
600 if allowed_to_change_status and status:
601 old_calculated_status = pull_request.calculated_review_status()
601 old_calculated_status = pull_request.calculated_review_status()
602 ChangesetStatusModel().set_status(
602 ChangesetStatusModel().set_status(
603 pull_request.target_repo.repo_id,
603 pull_request.target_repo.repo_id,
604 status,
604 status,
605 apiuser.user_id,
605 apiuser.user_id,
606 comment,
606 comment,
607 pull_request=pull_request.pull_request_id
607 pull_request=pull_request.pull_request_id
608 )
608 )
609 Session().flush()
609 Session().flush()
610
610
611 Session().commit()
611 Session().commit()
612
612
613 PullRequestModel().trigger_pull_request_hook(
613 PullRequestModel().trigger_pull_request_hook(
614 pull_request, apiuser, 'comment',
614 pull_request, apiuser, 'comment',
615 data={'comment': comment})
615 data={'comment': comment})
616
616
617 if allowed_to_change_status and status:
617 if allowed_to_change_status and status:
618 # we now calculate the status of pull request, and based on that
618 # we now calculate the status of pull request, and based on that
619 # calculation we set the commits status
619 # calculation we set the commits status
620 calculated_status = pull_request.calculated_review_status()
620 calculated_status = pull_request.calculated_review_status()
621 if old_calculated_status != calculated_status:
621 if old_calculated_status != calculated_status:
622 PullRequestModel().trigger_pull_request_hook(
622 PullRequestModel().trigger_pull_request_hook(
623 pull_request, apiuser, 'review_status_change',
623 pull_request, apiuser, 'review_status_change',
624 data={'status': calculated_status})
624 data={'status': calculated_status})
625
625
626 data = {
626 data = {
627 'pull_request_id': pull_request.pull_request_id,
627 'pull_request_id': pull_request.pull_request_id,
628 'comment_id': comment.comment_id if comment else None,
628 'comment_id': comment.comment_id if comment else None,
629 'status': {'given': status, 'was_changed': status_change},
629 'status': {'given': status, 'was_changed': status_change},
630 }
630 }
631 return data
631 return data
632
632
633
633
634 @jsonrpc_method()
634 @jsonrpc_method()
635 def create_pull_request(
635 def create_pull_request(
636 request, apiuser, source_repo, target_repo, source_ref, target_ref,
636 request, apiuser, source_repo, target_repo, source_ref, target_ref,
637 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
637 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
638 description_renderer=Optional(''), reviewers=Optional(None)):
638 description_renderer=Optional(''), reviewers=Optional(None)):
639 """
639 """
640 Creates a new pull request.
640 Creates a new pull request.
641
641
642 Accepts refs in the following formats:
642 Accepts refs in the following formats:
643
643
644 * branch:<branch_name>:<sha>
644 * branch:<branch_name>:<sha>
645 * branch:<branch_name>
645 * branch:<branch_name>
646 * bookmark:<bookmark_name>:<sha> (Mercurial only)
646 * bookmark:<bookmark_name>:<sha> (Mercurial only)
647 * bookmark:<bookmark_name> (Mercurial only)
647 * bookmark:<bookmark_name> (Mercurial only)
648
648
649 :param apiuser: This is filled automatically from the |authtoken|.
649 :param apiuser: This is filled automatically from the |authtoken|.
650 :type apiuser: AuthUser
650 :type apiuser: AuthUser
651 :param source_repo: Set the source repository name.
651 :param source_repo: Set the source repository name.
652 :type source_repo: str
652 :type source_repo: str
653 :param target_repo: Set the target repository name.
653 :param target_repo: Set the target repository name.
654 :type target_repo: str
654 :type target_repo: str
655 :param source_ref: Set the source ref name.
655 :param source_ref: Set the source ref name.
656 :type source_ref: str
656 :type source_ref: str
657 :param target_ref: Set the target ref name.
657 :param target_ref: Set the target ref name.
658 :type target_ref: str
658 :type target_ref: str
659 :param owner: user_id or username
659 :param owner: user_id or username
660 :type owner: Optional(str)
660 :type owner: Optional(str)
661 :param title: Optionally Set the pull request title, it's generated otherwise
661 :param title: Optionally Set the pull request title, it's generated otherwise
662 :type title: str
662 :type title: str
663 :param description: Set the pull request description.
663 :param description: Set the pull request description.
664 :type description: Optional(str)
664 :type description: Optional(str)
665 :type description_renderer: Optional(str)
665 :type description_renderer: Optional(str)
666 :param description_renderer: Set pull request renderer for the description.
666 :param description_renderer: Set pull request renderer for the description.
667 It should be 'rst', 'markdown' or 'plain'. If not give default
667 It should be 'rst', 'markdown' or 'plain'. If not give default
668 system renderer will be used
668 system renderer will be used
669 :param reviewers: Set the new pull request reviewers list.
669 :param reviewers: Set the new pull request reviewers list.
670 Reviewer defined by review rules will be added automatically to the
670 Reviewer defined by review rules will be added automatically to the
671 defined list.
671 defined list.
672 :type reviewers: Optional(list)
672 :type reviewers: Optional(list)
673 Accepts username strings or objects of the format:
673 Accepts username strings or objects of the format:
674
674
675 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
675 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
676 """
676 """
677
677
678 source_db_repo = get_repo_or_error(source_repo)
678 source_db_repo = get_repo_or_error(source_repo)
679 target_db_repo = get_repo_or_error(target_repo)
679 target_db_repo = get_repo_or_error(target_repo)
680 if not has_superadmin_permission(apiuser):
680 if not has_superadmin_permission(apiuser):
681 _perms = ('repository.admin', 'repository.write', 'repository.read',)
681 _perms = ('repository.admin', 'repository.write', 'repository.read',)
682 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
682 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
683
683
684 owner = validate_set_owner_permissions(apiuser, owner)
684 owner = validate_set_owner_permissions(apiuser, owner)
685
685
686 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
686 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
687 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
687 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
688
688
689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
690 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
690 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
691
691
692 reviewer_objects = Optional.extract(reviewers) or []
692 reviewer_objects = Optional.extract(reviewers) or []
693
693
694 # serialize and validate passed in given reviewers
694 # serialize and validate passed in given reviewers
695 if reviewer_objects:
695 if reviewer_objects:
696 schema = ReviewerListSchema()
696 schema = ReviewerListSchema()
697 try:
697 try:
698 reviewer_objects = schema.deserialize(reviewer_objects)
698 reviewer_objects = schema.deserialize(reviewer_objects)
699 except Invalid as err:
699 except Invalid as err:
700 raise JSONRPCValidationError(colander_exc=err)
700 raise JSONRPCValidationError(colander_exc=err)
701
701
702 # validate users
702 # validate users
703 for reviewer_object in reviewer_objects:
703 for reviewer_object in reviewer_objects:
704 user = get_user_or_error(reviewer_object['username'])
704 user = get_user_or_error(reviewer_object['username'])
705 reviewer_object['user_id'] = user.user_id
705 reviewer_object['user_id'] = user.user_id
706
706
707 get_default_reviewers_data, validate_default_reviewers = \
707 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
708 PullRequestModel().get_reviewer_functions()
708 PullRequestModel().get_reviewer_functions()
709
709
710 # recalculate reviewers logic, to make sure we can validate this
710 # recalculate reviewers logic, to make sure we can validate this
711 default_reviewers_data = get_default_reviewers_data(
711 default_reviewers_data = get_default_reviewers_data(
712 owner, source_db_repo,
712 owner, source_db_repo,
713 source_commit, target_db_repo, target_commit)
713 source_commit, target_db_repo, target_commit)
714
714
715 # now MERGE our given with the calculated
715 # now MERGE our given with the calculated
716 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
716 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
717
717
718 try:
718 try:
719 reviewers = validate_default_reviewers(
719 reviewers = validate_default_reviewers(
720 reviewer_objects, default_reviewers_data)
720 reviewer_objects, default_reviewers_data)
721 except ValueError as e:
721 except ValueError as e:
722 raise JSONRPCError('Reviewers Validation: {}'.format(e))
722 raise JSONRPCError('Reviewers Validation: {}'.format(e))
723
723
724 title = Optional.extract(title)
724 title = Optional.extract(title)
725 if not title:
725 if not title:
726 title_source_ref = source_ref.split(':', 2)[1]
726 title_source_ref = source_ref.split(':', 2)[1]
727 title = PullRequestModel().generate_pullrequest_title(
727 title = PullRequestModel().generate_pullrequest_title(
728 source=source_repo,
728 source=source_repo,
729 source_ref=title_source_ref,
729 source_ref=title_source_ref,
730 target=target_repo
730 target=target_repo
731 )
731 )
732
732
733 diff_info = default_reviewers_data['diff_info']
733 diff_info = default_reviewers_data['diff_info']
734 common_ancestor_id = diff_info['ancestor']
734 common_ancestor_id = diff_info['ancestor']
735 commits = diff_info['commits']
735 commits = diff_info['commits']
736
736
737 if not common_ancestor_id:
737 if not common_ancestor_id:
738 raise JSONRPCError('no common ancestor found')
738 raise JSONRPCError('no common ancestor found')
739
739
740 if not commits:
740 if not commits:
741 raise JSONRPCError('no commits found')
741 raise JSONRPCError('no commits found')
742
742
743 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
743 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
744 revisions = [commit.raw_id for commit in reversed(commits)]
744 revisions = [commit.raw_id for commit in reversed(commits)]
745
745
746 # recalculate target ref based on ancestor
746 # recalculate target ref based on ancestor
747 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
747 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
748 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
748 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
749
749
750 # fetch renderer, if set fallback to plain in case of PR
750 # fetch renderer, if set fallback to plain in case of PR
751 rc_config = SettingsModel().get_all_settings()
751 rc_config = SettingsModel().get_all_settings()
752 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
752 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
753 description = Optional.extract(description)
753 description = Optional.extract(description)
754 description_renderer = Optional.extract(description_renderer) or default_system_renderer
754 description_renderer = Optional.extract(description_renderer) or default_system_renderer
755
755
756 pull_request = PullRequestModel().create(
756 pull_request = PullRequestModel().create(
757 created_by=owner.user_id,
757 created_by=owner.user_id,
758 source_repo=source_repo,
758 source_repo=source_repo,
759 source_ref=full_source_ref,
759 source_ref=full_source_ref,
760 target_repo=target_repo,
760 target_repo=target_repo,
761 target_ref=full_target_ref,
761 target_ref=full_target_ref,
762 common_ancestor_id=common_ancestor_id,
762 common_ancestor_id=common_ancestor_id,
763 revisions=revisions,
763 revisions=revisions,
764 reviewers=reviewers,
764 reviewers=reviewers,
765 title=title,
765 title=title,
766 description=description,
766 description=description,
767 description_renderer=description_renderer,
767 description_renderer=description_renderer,
768 reviewer_data=default_reviewers_data,
768 reviewer_data=default_reviewers_data,
769 auth_user=apiuser
769 auth_user=apiuser
770 )
770 )
771
771
772 Session().commit()
772 Session().commit()
773 data = {
773 data = {
774 'msg': 'Created new pull request `{}`'.format(title),
774 'msg': 'Created new pull request `{}`'.format(title),
775 'pull_request_id': pull_request.pull_request_id,
775 'pull_request_id': pull_request.pull_request_id,
776 }
776 }
777 return data
777 return data
778
778
779
779
780 @jsonrpc_method()
780 @jsonrpc_method()
781 def update_pull_request(
781 def update_pull_request(
782 request, apiuser, pullrequestid, repoid=Optional(None),
782 request, apiuser, pullrequestid, repoid=Optional(None),
783 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
783 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
784 reviewers=Optional(None), update_commits=Optional(None)):
784 reviewers=Optional(None), update_commits=Optional(None)):
785 """
785 """
786 Updates a pull request.
786 Updates a pull request.
787
787
788 :param apiuser: This is filled automatically from the |authtoken|.
788 :param apiuser: This is filled automatically from the |authtoken|.
789 :type apiuser: AuthUser
789 :type apiuser: AuthUser
790 :param repoid: Optional repository name or repository ID.
790 :param repoid: Optional repository name or repository ID.
791 :type repoid: str or int
791 :type repoid: str or int
792 :param pullrequestid: The pull request ID.
792 :param pullrequestid: The pull request ID.
793 :type pullrequestid: int
793 :type pullrequestid: int
794 :param title: Set the pull request title.
794 :param title: Set the pull request title.
795 :type title: str
795 :type title: str
796 :param description: Update pull request description.
796 :param description: Update pull request description.
797 :type description: Optional(str)
797 :type description: Optional(str)
798 :type description_renderer: Optional(str)
798 :type description_renderer: Optional(str)
799 :param description_renderer: Update pull request renderer for the description.
799 :param description_renderer: Update pull request renderer for the description.
800 It should be 'rst', 'markdown' or 'plain'
800 It should be 'rst', 'markdown' or 'plain'
801 :param reviewers: Update pull request reviewers list with new value.
801 :param reviewers: Update pull request reviewers list with new value.
802 :type reviewers: Optional(list)
802 :type reviewers: Optional(list)
803 Accepts username strings or objects of the format:
803 Accepts username strings or objects of the format:
804
804
805 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
805 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
806
806
807 :param update_commits: Trigger update of commits for this pull request
807 :param update_commits: Trigger update of commits for this pull request
808 :type: update_commits: Optional(bool)
808 :type: update_commits: Optional(bool)
809
809
810 Example output:
810 Example output:
811
811
812 .. code-block:: bash
812 .. code-block:: bash
813
813
814 id : <id_given_in_input>
814 id : <id_given_in_input>
815 result : {
815 result : {
816 "msg": "Updated pull request `63`",
816 "msg": "Updated pull request `63`",
817 "pull_request": <pull_request_object>,
817 "pull_request": <pull_request_object>,
818 "updated_reviewers": {
818 "updated_reviewers": {
819 "added": [
819 "added": [
820 "username"
820 "username"
821 ],
821 ],
822 "removed": []
822 "removed": []
823 },
823 },
824 "updated_commits": {
824 "updated_commits": {
825 "added": [
825 "added": [
826 "<sha1_hash>"
826 "<sha1_hash>"
827 ],
827 ],
828 "common": [
828 "common": [
829 "<sha1_hash>",
829 "<sha1_hash>",
830 "<sha1_hash>",
830 "<sha1_hash>",
831 ],
831 ],
832 "removed": []
832 "removed": []
833 }
833 }
834 }
834 }
835 error : null
835 error : null
836 """
836 """
837
837
838 pull_request = get_pull_request_or_error(pullrequestid)
838 pull_request = get_pull_request_or_error(pullrequestid)
839 if Optional.extract(repoid):
839 if Optional.extract(repoid):
840 repo = get_repo_or_error(repoid)
840 repo = get_repo_or_error(repoid)
841 else:
841 else:
842 repo = pull_request.target_repo
842 repo = pull_request.target_repo
843
843
844 if not PullRequestModel().check_user_update(
844 if not PullRequestModel().check_user_update(
845 pull_request, apiuser, api=True):
845 pull_request, apiuser, api=True):
846 raise JSONRPCError(
846 raise JSONRPCError(
847 'pull request `%s` update failed, no permission to update.' % (
847 'pull request `%s` update failed, no permission to update.' % (
848 pullrequestid,))
848 pullrequestid,))
849 if pull_request.is_closed():
849 if pull_request.is_closed():
850 raise JSONRPCError(
850 raise JSONRPCError(
851 'pull request `%s` update failed, pull request is closed' % (
851 'pull request `%s` update failed, pull request is closed' % (
852 pullrequestid,))
852 pullrequestid,))
853
853
854 reviewer_objects = Optional.extract(reviewers) or []
854 reviewer_objects = Optional.extract(reviewers) or []
855
855
856 if reviewer_objects:
856 if reviewer_objects:
857 schema = ReviewerListSchema()
857 schema = ReviewerListSchema()
858 try:
858 try:
859 reviewer_objects = schema.deserialize(reviewer_objects)
859 reviewer_objects = schema.deserialize(reviewer_objects)
860 except Invalid as err:
860 except Invalid as err:
861 raise JSONRPCValidationError(colander_exc=err)
861 raise JSONRPCValidationError(colander_exc=err)
862
862
863 # validate users
863 # validate users
864 for reviewer_object in reviewer_objects:
864 for reviewer_object in reviewer_objects:
865 user = get_user_or_error(reviewer_object['username'])
865 user = get_user_or_error(reviewer_object['username'])
866 reviewer_object['user_id'] = user.user_id
866 reviewer_object['user_id'] = user.user_id
867
867
868 get_default_reviewers_data, get_validated_reviewers = \
868 get_default_reviewers_data, get_validated_reviewers, validate_observers = \
869 PullRequestModel().get_reviewer_functions()
869 PullRequestModel().get_reviewer_functions()
870
870
871 # re-use stored rules
871 # re-use stored rules
872 reviewer_rules = pull_request.reviewer_data
872 reviewer_rules = pull_request.reviewer_data
873 try:
873 try:
874 reviewers = get_validated_reviewers(
874 reviewers = get_validated_reviewers(reviewer_objects, reviewer_rules)
875 reviewer_objects, reviewer_rules)
876 except ValueError as e:
875 except ValueError as e:
877 raise JSONRPCError('Reviewers Validation: {}'.format(e))
876 raise JSONRPCError('Reviewers Validation: {}'.format(e))
878 else:
877 else:
879 reviewers = []
878 reviewers = []
880
879
881 title = Optional.extract(title)
880 title = Optional.extract(title)
882 description = Optional.extract(description)
881 description = Optional.extract(description)
883 description_renderer = Optional.extract(description_renderer)
882 description_renderer = Optional.extract(description_renderer)
884
883
885 if title or description:
884 if title or description:
886 PullRequestModel().edit(
885 PullRequestModel().edit(
887 pull_request,
886 pull_request,
888 title or pull_request.title,
887 title or pull_request.title,
889 description or pull_request.description,
888 description or pull_request.description,
890 description_renderer or pull_request.description_renderer,
889 description_renderer or pull_request.description_renderer,
891 apiuser)
890 apiuser)
892 Session().commit()
891 Session().commit()
893
892
894 commit_changes = {"added": [], "common": [], "removed": []}
893 commit_changes = {"added": [], "common": [], "removed": []}
895 if str2bool(Optional.extract(update_commits)):
894 if str2bool(Optional.extract(update_commits)):
896
895
897 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
896 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
898 raise JSONRPCError(
897 raise JSONRPCError(
899 'Operation forbidden because pull request is in state {}, '
898 'Operation forbidden because pull request is in state {}, '
900 'only state {} is allowed.'.format(
899 'only state {} is allowed.'.format(
901 pull_request.pull_request_state, PullRequest.STATE_CREATED))
900 pull_request.pull_request_state, PullRequest.STATE_CREATED))
902
901
903 with pull_request.set_state(PullRequest.STATE_UPDATING):
902 with pull_request.set_state(PullRequest.STATE_UPDATING):
904 if PullRequestModel().has_valid_update_type(pull_request):
903 if PullRequestModel().has_valid_update_type(pull_request):
905 db_user = apiuser.get_instance()
904 db_user = apiuser.get_instance()
906 update_response = PullRequestModel().update_commits(
905 update_response = PullRequestModel().update_commits(
907 pull_request, db_user)
906 pull_request, db_user)
908 commit_changes = update_response.changes or commit_changes
907 commit_changes = update_response.changes or commit_changes
909 Session().commit()
908 Session().commit()
910
909
911 reviewers_changes = {"added": [], "removed": []}
910 reviewers_changes = {"added": [], "removed": []}
912 if reviewers:
911 if reviewers:
913 old_calculated_status = pull_request.calculated_review_status()
912 old_calculated_status = pull_request.calculated_review_status()
914 added_reviewers, removed_reviewers = \
913 added_reviewers, removed_reviewers = \
915 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
914 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
916
915
917 reviewers_changes['added'] = sorted(
916 reviewers_changes['added'] = sorted(
918 [get_user_or_error(n).username for n in added_reviewers])
917 [get_user_or_error(n).username for n in added_reviewers])
919 reviewers_changes['removed'] = sorted(
918 reviewers_changes['removed'] = sorted(
920 [get_user_or_error(n).username for n in removed_reviewers])
919 [get_user_or_error(n).username for n in removed_reviewers])
921 Session().commit()
920 Session().commit()
922
921
923 # trigger status changed if change in reviewers changes the status
922 # trigger status changed if change in reviewers changes the status
924 calculated_status = pull_request.calculated_review_status()
923 calculated_status = pull_request.calculated_review_status()
925 if old_calculated_status != calculated_status:
924 if old_calculated_status != calculated_status:
926 PullRequestModel().trigger_pull_request_hook(
925 PullRequestModel().trigger_pull_request_hook(
927 pull_request, apiuser, 'review_status_change',
926 pull_request, apiuser, 'review_status_change',
928 data={'status': calculated_status})
927 data={'status': calculated_status})
929
928
930 data = {
929 data = {
931 'msg': 'Updated pull request `{}`'.format(
930 'msg': 'Updated pull request `{}`'.format(
932 pull_request.pull_request_id),
931 pull_request.pull_request_id),
933 'pull_request': pull_request.get_api_data(),
932 'pull_request': pull_request.get_api_data(),
934 'updated_commits': commit_changes,
933 'updated_commits': commit_changes,
935 'updated_reviewers': reviewers_changes
934 'updated_reviewers': reviewers_changes
936 }
935 }
937
936
938 return data
937 return data
939
938
940
939
941 @jsonrpc_method()
940 @jsonrpc_method()
942 def close_pull_request(
941 def close_pull_request(
943 request, apiuser, pullrequestid, repoid=Optional(None),
942 request, apiuser, pullrequestid, repoid=Optional(None),
944 userid=Optional(OAttr('apiuser')), message=Optional('')):
943 userid=Optional(OAttr('apiuser')), message=Optional('')):
945 """
944 """
946 Close the pull request specified by `pullrequestid`.
945 Close the pull request specified by `pullrequestid`.
947
946
948 :param apiuser: This is filled automatically from the |authtoken|.
947 :param apiuser: This is filled automatically from the |authtoken|.
949 :type apiuser: AuthUser
948 :type apiuser: AuthUser
950 :param repoid: Repository name or repository ID to which the pull
949 :param repoid: Repository name or repository ID to which the pull
951 request belongs.
950 request belongs.
952 :type repoid: str or int
951 :type repoid: str or int
953 :param pullrequestid: ID of the pull request to be closed.
952 :param pullrequestid: ID of the pull request to be closed.
954 :type pullrequestid: int
953 :type pullrequestid: int
955 :param userid: Close the pull request as this user.
954 :param userid: Close the pull request as this user.
956 :type userid: Optional(str or int)
955 :type userid: Optional(str or int)
957 :param message: Optional message to close the Pull Request with. If not
956 :param message: Optional message to close the Pull Request with. If not
958 specified it will be generated automatically.
957 specified it will be generated automatically.
959 :type message: Optional(str)
958 :type message: Optional(str)
960
959
961 Example output:
960 Example output:
962
961
963 .. code-block:: bash
962 .. code-block:: bash
964
963
965 "id": <id_given_in_input>,
964 "id": <id_given_in_input>,
966 "result": {
965 "result": {
967 "pull_request_id": "<int>",
966 "pull_request_id": "<int>",
968 "close_status": "<str:status_lbl>,
967 "close_status": "<str:status_lbl>,
969 "closed": "<bool>"
968 "closed": "<bool>"
970 },
969 },
971 "error": null
970 "error": null
972
971
973 """
972 """
974 _ = request.translate
973 _ = request.translate
975
974
976 pull_request = get_pull_request_or_error(pullrequestid)
975 pull_request = get_pull_request_or_error(pullrequestid)
977 if Optional.extract(repoid):
976 if Optional.extract(repoid):
978 repo = get_repo_or_error(repoid)
977 repo = get_repo_or_error(repoid)
979 else:
978 else:
980 repo = pull_request.target_repo
979 repo = pull_request.target_repo
981
980
982 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
981 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
983 user=apiuser, repo_name=repo.repo_name)
982 user=apiuser, repo_name=repo.repo_name)
984 if not isinstance(userid, Optional):
983 if not isinstance(userid, Optional):
985 if has_superadmin_permission(apiuser) or is_repo_admin:
984 if has_superadmin_permission(apiuser) or is_repo_admin:
986 apiuser = get_user_or_error(userid)
985 apiuser = get_user_or_error(userid)
987 else:
986 else:
988 raise JSONRPCError('userid is not the same as your user')
987 raise JSONRPCError('userid is not the same as your user')
989
988
990 if pull_request.is_closed():
989 if pull_request.is_closed():
991 raise JSONRPCError(
990 raise JSONRPCError(
992 'pull request `%s` is already closed' % (pullrequestid,))
991 'pull request `%s` is already closed' % (pullrequestid,))
993
992
994 # only owner or admin or person with write permissions
993 # only owner or admin or person with write permissions
995 allowed_to_close = PullRequestModel().check_user_update(
994 allowed_to_close = PullRequestModel().check_user_update(
996 pull_request, apiuser, api=True)
995 pull_request, apiuser, api=True)
997
996
998 if not allowed_to_close:
997 if not allowed_to_close:
999 raise JSONRPCError(
998 raise JSONRPCError(
1000 'pull request `%s` close failed, no permission to close.' % (
999 'pull request `%s` close failed, no permission to close.' % (
1001 pullrequestid,))
1000 pullrequestid,))
1002
1001
1003 # message we're using to close the PR, else it's automatically generated
1002 # message we're using to close the PR, else it's automatically generated
1004 message = Optional.extract(message)
1003 message = Optional.extract(message)
1005
1004
1006 # finally close the PR, with proper message comment
1005 # finally close the PR, with proper message comment
1007 comment, status = PullRequestModel().close_pull_request_with_comment(
1006 comment, status = PullRequestModel().close_pull_request_with_comment(
1008 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1007 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1009 status_lbl = ChangesetStatus.get_status_lbl(status)
1008 status_lbl = ChangesetStatus.get_status_lbl(status)
1010
1009
1011 Session().commit()
1010 Session().commit()
1012
1011
1013 data = {
1012 data = {
1014 'pull_request_id': pull_request.pull_request_id,
1013 'pull_request_id': pull_request.pull_request_id,
1015 'close_status': status_lbl,
1014 'close_status': status_lbl,
1016 'closed': True,
1015 'closed': True,
1017 }
1016 }
1018 return data
1017 return data
@@ -1,418 +1,486 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-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 import os
21 import os
22 import logging
22 import logging
23 import datetime
23 import datetime
24
24
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render_to_response
26 from pyramid.renderers import render_to_response
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib.celerylib import run_task, tasks
28 from rhodecode.lib.celerylib import run_task, tasks
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.model.db import User
30 from rhodecode.model.db import User
31 from rhodecode.model.notification import EmailNotificationModel
31 from rhodecode.model.notification import EmailNotificationModel
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class DebugStyleView(BaseAppView):
36 class DebugStyleView(BaseAppView):
37
37 def load_default_context(self):
38 def load_default_context(self):
38 c = self._get_local_tmpl_context()
39 c = self._get_local_tmpl_context()
39
40
40 return c
41 return c
41
42
42 @view_config(
43 @view_config(
43 route_name='debug_style_home', request_method='GET',
44 route_name='debug_style_home', request_method='GET',
44 renderer=None)
45 renderer=None)
45 def index(self):
46 def index(self):
46 c = self.load_default_context()
47 c = self.load_default_context()
47 c.active = 'index'
48 c.active = 'index'
48
49
49 return render_to_response(
50 return render_to_response(
50 'debug_style/index.html', self._get_template_context(c),
51 'debug_style/index.html', self._get_template_context(c),
51 request=self.request)
52 request=self.request)
52
53
53 @view_config(
54 @view_config(
54 route_name='debug_style_email', request_method='GET',
55 route_name='debug_style_email', request_method='GET',
55 renderer=None)
56 renderer=None)
56 @view_config(
57 @view_config(
57 route_name='debug_style_email_plain_rendered', request_method='GET',
58 route_name='debug_style_email_plain_rendered', request_method='GET',
58 renderer=None)
59 renderer=None)
59 def render_email(self):
60 def render_email(self):
60 c = self.load_default_context()
61 c = self.load_default_context()
61 email_id = self.request.matchdict['email_id']
62 email_id = self.request.matchdict['email_id']
62 c.active = 'emails'
63 c.active = 'emails'
63
64
64 pr = AttributeDict(
65 pr = AttributeDict(
65 pull_request_id=123,
66 pull_request_id=123,
66 title='digital_ocean: fix redis, elastic search start on boot, '
67 title='digital_ocean: fix redis, elastic search start on boot, '
67 'fix fd limits on supervisor, set postgres 11 version',
68 'fix fd limits on supervisor, set postgres 11 version',
68 description='''
69 description='''
69 Check if we should use full-topic or mini-topic.
70 Check if we should use full-topic or mini-topic.
70
71
71 - full topic produces some problems with merge states etc
72 - full topic produces some problems with merge states etc
72 - server-mini-topic needs probably tweeks.
73 - server-mini-topic needs probably tweeks.
73 ''',
74 ''',
74 repo_name='foobar',
75 repo_name='foobar',
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
77 target_ref_parts=AttributeDict(type='branch', name='master'),
77 )
78 )
79
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
80 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
81 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
82 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
81 # file/commit changes for PR update
83 # file/commit changes for PR update
82 commit_changes = AttributeDict({
84 commit_changes = AttributeDict({
83 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
85 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
84 'removed': ['eeeeeeeeeee'],
86 'removed': ['eeeeeeeeeee'],
85 })
87 })
88
86 file_changes = AttributeDict({
89 file_changes = AttributeDict({
87 'added': ['a/file1.md', 'file2.py'],
90 'added': ['a/file1.md', 'file2.py'],
88 'modified': ['b/modified_file.rst'],
91 'modified': ['b/modified_file.rst'],
89 'removed': ['.idea'],
92 'removed': ['.idea'],
90 })
93 })
91
94
92 exc_traceback = {
95 exc_traceback = {
93 'exc_utc_date': '2020-03-26T12:54:50.683281',
96 'exc_utc_date': '2020-03-26T12:54:50.683281',
94 'exc_id': 139638856342656,
97 'exc_id': 139638856342656,
95 'exc_timestamp': '1585227290.683288',
98 'exc_timestamp': '1585227290.683288',
96 'version': 'v1',
99 'version': 'v1',
97 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
100 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
98 'exc_type': 'AttributeError'
101 'exc_type': 'AttributeError'
99 }
102 }
103
100 email_kwargs = {
104 email_kwargs = {
101 'test': {},
105 'test': {},
106
102 'message': {
107 'message': {
103 'body': 'message body !'
108 'body': 'message body !'
104 },
109 },
110
105 'email_test': {
111 'email_test': {
106 'user': user,
112 'user': user,
107 'date': datetime.datetime.now(),
113 'date': datetime.datetime.now(),
108 },
114 },
115
109 'exception': {
116 'exception': {
110 'email_prefix': '[RHODECODE ERROR]',
117 'email_prefix': '[RHODECODE ERROR]',
111 'exc_id': exc_traceback['exc_id'],
118 'exc_id': exc_traceback['exc_id'],
112 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
119 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
113 'exc_type_name': 'NameError',
120 'exc_type_name': 'NameError',
114 'exc_traceback': exc_traceback,
121 'exc_traceback': exc_traceback,
115 },
122 },
123
116 'password_reset': {
124 'password_reset': {
117 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
125 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
118
126
119 'user': user,
127 'user': user,
120 'date': datetime.datetime.now(),
128 'date': datetime.datetime.now(),
121 'email': 'test@rhodecode.com',
129 'email': 'test@rhodecode.com',
122 'first_admin_email': User.get_first_super_admin().email
130 'first_admin_email': User.get_first_super_admin().email
123 },
131 },
132
124 'password_reset_confirmation': {
133 'password_reset_confirmation': {
125 'new_password': 'new-password-example',
134 'new_password': 'new-password-example',
126 'user': user,
135 'user': user,
127 'date': datetime.datetime.now(),
136 'date': datetime.datetime.now(),
128 'email': 'test@rhodecode.com',
137 'email': 'test@rhodecode.com',
129 'first_admin_email': User.get_first_super_admin().email
138 'first_admin_email': User.get_first_super_admin().email
130 },
139 },
140
131 'registration': {
141 'registration': {
132 'user': user,
142 'user': user,
133 'date': datetime.datetime.now(),
143 'date': datetime.datetime.now(),
134 },
144 },
135
145
136 'pull_request_comment': {
146 'pull_request_comment': {
137 'user': user,
147 'user': user,
138
148
139 'status_change': None,
149 'status_change': None,
140 'status_change_type': None,
150 'status_change_type': None,
141
151
142 'pull_request': pr,
152 'pull_request': pr,
143 'pull_request_commits': [],
153 'pull_request_commits': [],
144
154
145 'pull_request_target_repo': target_repo,
155 'pull_request_target_repo': target_repo,
146 'pull_request_target_repo_url': 'http://target-repo/url',
156 'pull_request_target_repo_url': 'http://target-repo/url',
147
157
148 'pull_request_source_repo': source_repo,
158 'pull_request_source_repo': source_repo,
149 'pull_request_source_repo_url': 'http://source-repo/url',
159 'pull_request_source_repo_url': 'http://source-repo/url',
150
160
151 'pull_request_url': 'http://localhost/pr1',
161 'pull_request_url': 'http://localhost/pr1',
152 'pr_comment_url': 'http://comment-url',
162 'pr_comment_url': 'http://comment-url',
153 'pr_comment_reply_url': 'http://comment-url#reply',
163 'pr_comment_reply_url': 'http://comment-url#reply',
154
164
155 'comment_file': None,
165 'comment_file': None,
156 'comment_line': None,
166 'comment_line': None,
157 'comment_type': 'note',
167 'comment_type': 'note',
158 'comment_body': 'This is my comment body. *I like !*',
168 'comment_body': 'This is my comment body. *I like !*',
159 'comment_id': 2048,
169 'comment_id': 2048,
160 'renderer_type': 'markdown',
170 'renderer_type': 'markdown',
161 'mention': True,
171 'mention': True,
162
172
163 },
173 },
174
164 'pull_request_comment+status': {
175 'pull_request_comment+status': {
165 'user': user,
176 'user': user,
166
177
167 'status_change': 'approved',
178 'status_change': 'approved',
168 'status_change_type': 'approved',
179 'status_change_type': 'approved',
169
180
170 'pull_request': pr,
181 'pull_request': pr,
171 'pull_request_commits': [],
182 'pull_request_commits': [],
172
183
173 'pull_request_target_repo': target_repo,
184 'pull_request_target_repo': target_repo,
174 'pull_request_target_repo_url': 'http://target-repo/url',
185 'pull_request_target_repo_url': 'http://target-repo/url',
175
186
176 'pull_request_source_repo': source_repo,
187 'pull_request_source_repo': source_repo,
177 'pull_request_source_repo_url': 'http://source-repo/url',
188 'pull_request_source_repo_url': 'http://source-repo/url',
178
189
179 'pull_request_url': 'http://localhost/pr1',
190 'pull_request_url': 'http://localhost/pr1',
180 'pr_comment_url': 'http://comment-url',
191 'pr_comment_url': 'http://comment-url',
181 'pr_comment_reply_url': 'http://comment-url#reply',
192 'pr_comment_reply_url': 'http://comment-url#reply',
182
193
183 'comment_type': 'todo',
194 'comment_type': 'todo',
184 'comment_file': None,
195 'comment_file': None,
185 'comment_line': None,
196 'comment_line': None,
186 'comment_body': '''
197 'comment_body': '''
187 I think something like this would be better
198 I think something like this would be better
188
199
189 ```py
200 ```py
190 // markdown renderer
201 // markdown renderer
191
202
192 def db():
203 def db():
193 global connection
204 global connection
194 return connection
205 return connection
195
206
196 ```
207 ```
197
208
198 ''',
209 ''',
199 'comment_id': 2048,
210 'comment_id': 2048,
200 'renderer_type': 'markdown',
211 'renderer_type': 'markdown',
201 'mention': True,
212 'mention': True,
202
213
203 },
214 },
215
204 'pull_request_comment+file': {
216 'pull_request_comment+file': {
205 'user': user,
217 'user': user,
206
218
207 'status_change': None,
219 'status_change': None,
208 'status_change_type': None,
220 'status_change_type': None,
209
221
210 'pull_request': pr,
222 'pull_request': pr,
211 'pull_request_commits': [],
223 'pull_request_commits': [],
212
224
213 'pull_request_target_repo': target_repo,
225 'pull_request_target_repo': target_repo,
214 'pull_request_target_repo_url': 'http://target-repo/url',
226 'pull_request_target_repo_url': 'http://target-repo/url',
215
227
216 'pull_request_source_repo': source_repo,
228 'pull_request_source_repo': source_repo,
217 'pull_request_source_repo_url': 'http://source-repo/url',
229 'pull_request_source_repo_url': 'http://source-repo/url',
218
230
219 'pull_request_url': 'http://localhost/pr1',
231 'pull_request_url': 'http://localhost/pr1',
220
232
221 'pr_comment_url': 'http://comment-url',
233 'pr_comment_url': 'http://comment-url',
222 'pr_comment_reply_url': 'http://comment-url#reply',
234 'pr_comment_reply_url': 'http://comment-url#reply',
223
235
224 'comment_file': 'rhodecode/model/get_flow_commits',
236 'comment_file': 'rhodecode/model/get_flow_commits',
225 'comment_line': 'o1210',
237 'comment_line': 'o1210',
226 'comment_type': 'todo',
238 'comment_type': 'todo',
227 'comment_body': '''
239 'comment_body': '''
228 I like this !
240 I like this !
229
241
230 But please check this code
242 But please check this code
231
243
232 .. code-block:: javascript
244 .. code-block:: javascript
233
245
234 // THIS IS RST CODE
246 // THIS IS RST CODE
235
247
236 this.createResolutionComment = function(commentId) {
248 this.createResolutionComment = function(commentId) {
237 // hide the trigger text
249 // hide the trigger text
238 $('#resolve-comment-{0}'.format(commentId)).hide();
250 $('#resolve-comment-{0}'.format(commentId)).hide();
239
251
240 var comment = $('#comment-'+commentId);
252 var comment = $('#comment-'+commentId);
241 var commentData = comment.data();
253 var commentData = comment.data();
242 if (commentData.commentInline) {
254 if (commentData.commentInline) {
243 this.createComment(comment, commentId)
255 this.createComment(comment, commentId)
244 } else {
256 } else {
245 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
257 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
246 }
258 }
247
259
248 return false;
260 return false;
249 };
261 };
250
262
251 This should work better !
263 This should work better !
252 ''',
264 ''',
253 'comment_id': 2048,
265 'comment_id': 2048,
254 'renderer_type': 'rst',
266 'renderer_type': 'rst',
255 'mention': True,
267 'mention': True,
256
268
257 },
269 },
258
270
259 'pull_request_update': {
271 'pull_request_update': {
260 'updating_user': user,
272 'updating_user': user,
261
273
262 'status_change': None,
274 'status_change': None,
263 'status_change_type': None,
275 'status_change_type': None,
264
276
265 'pull_request': pr,
277 'pull_request': pr,
266 'pull_request_commits': [],
278 'pull_request_commits': [],
267
279
268 'pull_request_target_repo': target_repo,
280 'pull_request_target_repo': target_repo,
269 'pull_request_target_repo_url': 'http://target-repo/url',
281 'pull_request_target_repo_url': 'http://target-repo/url',
270
282
271 'pull_request_source_repo': source_repo,
283 'pull_request_source_repo': source_repo,
272 'pull_request_source_repo_url': 'http://source-repo/url',
284 'pull_request_source_repo_url': 'http://source-repo/url',
273
285
274 'pull_request_url': 'http://localhost/pr1',
286 'pull_request_url': 'http://localhost/pr1',
275
287
276 # update comment links
288 # update comment links
277 'pr_comment_url': 'http://comment-url',
289 'pr_comment_url': 'http://comment-url',
278 'pr_comment_reply_url': 'http://comment-url#reply',
290 'pr_comment_reply_url': 'http://comment-url#reply',
279 'ancestor_commit_id': 'f39bd443',
291 'ancestor_commit_id': 'f39bd443',
280 'added_commits': commit_changes.added,
292 'added_commits': commit_changes.added,
281 'removed_commits': commit_changes.removed,
293 'removed_commits': commit_changes.removed,
282 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
294 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
283 'added_files': file_changes.added,
295 'added_files': file_changes.added,
284 'modified_files': file_changes.modified,
296 'modified_files': file_changes.modified,
285 'removed_files': file_changes.removed,
297 'removed_files': file_changes.removed,
286 },
298 },
287
299
288 'cs_comment': {
300 'cs_comment': {
289 'user': user,
301 'user': user,
290 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
302 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
291 'status_change': None,
303 'status_change': None,
292 'status_change_type': None,
304 'status_change_type': None,
293
305
294 'commit_target_repo_url': 'http://foo.example.com/#comment1',
306 'commit_target_repo_url': 'http://foo.example.com/#comment1',
295 'repo_name': 'test-repo',
307 'repo_name': 'test-repo',
296 'comment_type': 'note',
308 'comment_type': 'note',
297 'comment_file': None,
309 'comment_file': None,
298 'comment_line': None,
310 'comment_line': None,
299 'commit_comment_url': 'http://comment-url',
311 'commit_comment_url': 'http://comment-url',
300 'commit_comment_reply_url': 'http://comment-url#reply',
312 'commit_comment_reply_url': 'http://comment-url#reply',
301 'comment_body': 'This is my comment body. *I like !*',
313 'comment_body': 'This is my comment body. *I like !*',
302 'comment_id': 2048,
314 'comment_id': 2048,
303 'renderer_type': 'markdown',
315 'renderer_type': 'markdown',
304 'mention': True,
316 'mention': True,
305 },
317 },
318
306 'cs_comment+status': {
319 'cs_comment+status': {
307 'user': user,
320 'user': user,
308 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
321 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
309 'status_change': 'approved',
322 'status_change': 'approved',
310 'status_change_type': 'approved',
323 'status_change_type': 'approved',
311
324
312 'commit_target_repo_url': 'http://foo.example.com/#comment1',
325 'commit_target_repo_url': 'http://foo.example.com/#comment1',
313 'repo_name': 'test-repo',
326 'repo_name': 'test-repo',
314 'comment_type': 'note',
327 'comment_type': 'note',
315 'comment_file': None,
328 'comment_file': None,
316 'comment_line': None,
329 'comment_line': None,
317 'commit_comment_url': 'http://comment-url',
330 'commit_comment_url': 'http://comment-url',
318 'commit_comment_reply_url': 'http://comment-url#reply',
331 'commit_comment_reply_url': 'http://comment-url#reply',
319 'comment_body': '''
332 'comment_body': '''
320 Hello **world**
333 Hello **world**
321
334
322 This is a multiline comment :)
335 This is a multiline comment :)
323
336
324 - list
337 - list
325 - list2
338 - list2
326 ''',
339 ''',
327 'comment_id': 2048,
340 'comment_id': 2048,
328 'renderer_type': 'markdown',
341 'renderer_type': 'markdown',
329 'mention': True,
342 'mention': True,
330 },
343 },
344
331 'cs_comment+file': {
345 'cs_comment+file': {
332 'user': user,
346 'user': user,
333 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
347 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
334 'status_change': None,
348 'status_change': None,
335 'status_change_type': None,
349 'status_change_type': None,
336
350
337 'commit_target_repo_url': 'http://foo.example.com/#comment1',
351 'commit_target_repo_url': 'http://foo.example.com/#comment1',
338 'repo_name': 'test-repo',
352 'repo_name': 'test-repo',
339
353
340 'comment_type': 'note',
354 'comment_type': 'note',
341 'comment_file': 'test-file.py',
355 'comment_file': 'test-file.py',
342 'comment_line': 'n100',
356 'comment_line': 'n100',
343
357
344 'commit_comment_url': 'http://comment-url',
358 'commit_comment_url': 'http://comment-url',
345 'commit_comment_reply_url': 'http://comment-url#reply',
359 'commit_comment_reply_url': 'http://comment-url#reply',
346 'comment_body': 'This is my comment body. *I like !*',
360 'comment_body': 'This is my comment body. *I like !*',
347 'comment_id': 2048,
361 'comment_id': 2048,
348 'renderer_type': 'markdown',
362 'renderer_type': 'markdown',
349 'mention': True,
363 'mention': True,
350 },
364 },
351
365
352 'pull_request': {
366 'pull_request': {
353 'user': user,
367 'user': user,
354 'pull_request': pr,
368 'pull_request': pr,
355 'pull_request_commits': [
369 'pull_request_commits': [
356 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
370 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
371 my-account: moved email closer to profile as it's similar data just moved outside.
372 '''),
373 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
374 users: description edit fixes
375
376 - tests
377 - added metatags info
378 '''),
379 ],
380
381 'pull_request_target_repo': target_repo,
382 'pull_request_target_repo_url': 'http://target-repo/url',
383
384 'pull_request_source_repo': source_repo,
385 'pull_request_source_repo_url': 'http://source-repo/url',
386
387 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
388 'user_role': 'reviewer',
389 },
390
391 'pull_request+reviewer_role': {
392 'user': user,
393 'pull_request': pr,
394 'pull_request_commits': [
395 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
357 my-account: moved email closer to profile as it's similar data just moved outside.
396 my-account: moved email closer to profile as it's similar data just moved outside.
358 '''),
397 '''),
359 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
398 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
360 users: description edit fixes
399 users: description edit fixes
361
400
362 - tests
401 - tests
363 - added metatags info
402 - added metatags info
364 '''),
403 '''),
365 ],
404 ],
366
405
367 'pull_request_target_repo': target_repo,
406 'pull_request_target_repo': target_repo,
368 'pull_request_target_repo_url': 'http://target-repo/url',
407 'pull_request_target_repo_url': 'http://target-repo/url',
369
408
370 'pull_request_source_repo': source_repo,
409 'pull_request_source_repo': source_repo,
371 'pull_request_source_repo_url': 'http://source-repo/url',
410 'pull_request_source_repo_url': 'http://source-repo/url',
372
411
373 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
412 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
413 'user_role': 'reviewer',
414 },
415
416 'pull_request+observer_role': {
417 'user': user,
418 'pull_request': pr,
419 'pull_request_commits': [
420 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
421 my-account: moved email closer to profile as it's similar data just moved outside.
422 '''),
423 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
424 users: description edit fixes
425
426 - tests
427 - added metatags info
428 '''),
429 ],
430
431 'pull_request_target_repo': target_repo,
432 'pull_request_target_repo_url': 'http://target-repo/url',
433
434 'pull_request_source_repo': source_repo,
435 'pull_request_source_repo_url': 'http://source-repo/url',
436
437 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
438 'user_role': 'observer'
374 }
439 }
375
376 }
440 }
377
441
378 template_type = email_id.split('+')[0]
442 template_type = email_id.split('+')[0]
379 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
443 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
380 template_type, **email_kwargs.get(email_id, {}))
444 template_type, **email_kwargs.get(email_id, {}))
381
445
382 test_email = self.request.GET.get('email')
446 test_email = self.request.GET.get('email')
383 if test_email:
447 if test_email:
384 recipients = [test_email]
448 recipients = [test_email]
385 run_task(tasks.send_email, recipients, c.subject,
449 run_task(tasks.send_email, recipients, c.subject,
386 c.email_body_plaintext, c.email_body)
450 c.email_body_plaintext, c.email_body)
387
451
388 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
452 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
389 template = 'debug_style/email_plain_rendered.mako'
453 template = 'debug_style/email_plain_rendered.mako'
390 else:
454 else:
391 template = 'debug_style/email.mako'
455 template = 'debug_style/email.mako'
392 return render_to_response(
456 return render_to_response(
393 template, self._get_template_context(c),
457 template, self._get_template_context(c),
394 request=self.request)
458 request=self.request)
395
459
396 @view_config(
460 @view_config(
397 route_name='debug_style_template', request_method='GET',
461 route_name='debug_style_template', request_method='GET',
398 renderer=None)
462 renderer=None)
399 def template(self):
463 def template(self):
400 t_path = self.request.matchdict['t_path']
464 t_path = self.request.matchdict['t_path']
401 c = self.load_default_context()
465 c = self.load_default_context()
402 c.active = os.path.splitext(t_path)[0]
466 c.active = os.path.splitext(t_path)[0]
403 c.came_from = ''
467 c.came_from = ''
468 # NOTE(marcink): extend the email types with variations based on data sets
404 c.email_types = {
469 c.email_types = {
405 'cs_comment+file': {},
470 'cs_comment+file': {},
406 'cs_comment+status': {},
471 'cs_comment+status': {},
407
472
408 'pull_request_comment+file': {},
473 'pull_request_comment+file': {},
409 'pull_request_comment+status': {},
474 'pull_request_comment+status': {},
410
475
411 'pull_request_update': {},
476 'pull_request_update': {},
477
478 'pull_request+reviewer_role': {},
479 'pull_request+observer_role': {},
412 }
480 }
413 c.email_types.update(EmailNotificationModel.email_types)
481 c.email_types.update(EmailNotificationModel.email_types)
414
482
415 return render_to_response(
483 return render_to_response(
416 'debug_style/' + t_path, self._get_template_context(c),
484 'debug_style/' + t_path, self._get_template_context(c),
417 request=self.request)
485 request=self.request)
418
486
@@ -1,87 +1,95 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-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 from rhodecode.lib import helpers as h
21 from rhodecode.lib import helpers as h
22 from rhodecode.lib.utils2 import safe_int
22 from rhodecode.lib.utils2 import safe_int
23 from rhodecode.model.pull_request import get_diff_info
23 from rhodecode.model.pull_request import get_diff_info
24
24 from rhodecode.model.db import PullRequestReviewers
25 REVIEWER_API_VERSION = 'V3'
25 # V3 - Reviewers, with default rules data
26 # v4 - Added observers metadata
27 REVIEWER_API_VERSION = 'V4'
26
28
27
29
28 def reviewer_as_json(user, reasons=None, mandatory=False, rules=None, user_group=None):
30 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
29 """
31 """
30 Returns json struct of a reviewer for frontend
32 Returns json struct of a reviewer for frontend
31
33
32 :param user: the reviewer
34 :param user: the reviewer
33 :param reasons: list of strings of why they are reviewers
35 :param reasons: list of strings of why they are reviewers
34 :param mandatory: bool, to set user as mandatory
36 :param mandatory: bool, to set user as mandatory
35 """
37 """
38 role = role or PullRequestReviewers.ROLE_REVIEWER
39 if role not in PullRequestReviewers.ROLES:
40 raise ValueError('role is not one of %s', PullRequestReviewers.ROLES)
36
41
37 return {
42 return {
38 'user_id': user.user_id,
43 'user_id': user.user_id,
39 'reasons': reasons or [],
44 'reasons': reasons or [],
40 'rules': rules or [],
45 'rules': rules or [],
46 'role': role,
41 'mandatory': mandatory,
47 'mandatory': mandatory,
42 'user_group': user_group,
48 'user_group': user_group,
43 'username': user.username,
49 'username': user.username,
44 'first_name': user.first_name,
50 'first_name': user.first_name,
45 'last_name': user.last_name,
51 'last_name': user.last_name,
46 'user_link': h.link_to_user(user),
52 'user_link': h.link_to_user(user),
47 'gravatar_link': h.gravatar_url(user.email, 14),
53 'gravatar_link': h.gravatar_url(user.email, 14),
48 }
54 }
49
55
50
56
51 def get_default_reviewers_data(
57 def get_default_reviewers_data(current_user, source_repo, source_commit, target_repo, target_commit):
52 current_user, source_repo, source_commit, target_repo, target_commit):
53 """
58 """
54 Return json for default reviewers of a repository
59 Return json for default reviewers of a repository
55 """
60 """
56
61
57 diff_info = get_diff_info(
62 diff_info = get_diff_info(
58 source_repo, source_commit.raw_id, target_repo, target_commit.raw_id)
63 source_repo, source_commit.raw_id, target_repo, target_commit.raw_id)
59
64
60 reasons = ['Default reviewer', 'Repository owner']
65 reasons = ['Default reviewer', 'Repository owner']
61 json_reviewers = [reviewer_as_json(
66 json_reviewers = [reviewer_as_json(
62 user=target_repo.user, reasons=reasons, mandatory=False, rules=None)]
67 user=target_repo.user, reasons=reasons, mandatory=False, rules=None, role=None)]
63
68
64 return {
69 return {
65 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
70 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
66 'diff_info': diff_info,
71 'diff_info': diff_info,
67 'reviewers': json_reviewers,
72 'reviewers': json_reviewers,
68 'rules': {},
73 'rules': {},
69 'rules_data': {},
74 'rules_data': {},
70 }
75 }
71
76
72
77
73 def validate_default_reviewers(review_members, reviewer_rules):
78 def validate_default_reviewers(review_members, reviewer_rules):
74 """
79 """
75 Function to validate submitted reviewers against the saved rules
80 Function to validate submitted reviewers against the saved rules
76
77 """
81 """
78 reviewers = []
82 reviewers = []
79 reviewer_by_id = {}
83 reviewer_by_id = {}
80 for r in review_members:
84 for r in review_members:
81 reviewer_user_id = safe_int(r['user_id'])
85 reviewer_user_id = safe_int(r['user_id'])
82 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['rules'])
86 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['role'], r['rules'])
83
87
84 reviewer_by_id[reviewer_user_id] = entry
88 reviewer_by_id[reviewer_user_id] = entry
85 reviewers.append(entry)
89 reviewers.append(entry)
86
90
87 return reviewers
91 return reviewers
92
93
94 def validate_observers(observer_members):
95 return {}
@@ -1,781 +1,781 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 import logging
21 import logging
22 import collections
22 import collections
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
92
92
93 try:
93 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
95 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
97 pre_load += ['hidden', 'obsolete', 'phase']
98
98
99 if len(commit_range) == 2:
99 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
100 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
101 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
102 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
103 commits = list(commits)
104 else:
104 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
106 commit_id=commit_id_range, pre_load=pre_load)]
107
107
108 c.commit_ranges = commits
108 c.commit_ranges = commits
109 if not c.commit_ranges:
109 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
110 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
111 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
113 h.flash(msg, category='error')
114 raise HTTPNotFound()
114 raise HTTPNotFound()
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
118 single_commit = len(c.commit_ranges) == 1
119
119
120 c.changes = OrderedDict()
120 c.changes = OrderedDict()
121 c.lines_added = 0
121 c.lines_added = 0
122 c.lines_deleted = 0
122 c.lines_deleted = 0
123
123
124 # auto collapse if we have more than limit
124 # auto collapse if we have more than limit
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127
127
128 c.commit_statuses = ChangesetStatus.STATUSES
128 c.commit_statuses = ChangesetStatus.STATUSES
129 c.inline_comments = []
129 c.inline_comments = []
130 c.files = []
130 c.files = []
131
131
132 c.comments = []
132 c.comments = []
133 c.unresolved_comments = []
133 c.unresolved_comments = []
134 c.resolved_comments = []
134 c.resolved_comments = []
135
135
136 # Single commit
136 # Single commit
137 if single_commit:
137 if single_commit:
138 commit = c.commit_ranges[0]
138 commit = c.commit_ranges[0]
139 c.comments = CommentsModel().get_comments(
139 c.comments = CommentsModel().get_comments(
140 self.db_repo.repo_id,
140 self.db_repo.repo_id,
141 revision=commit.raw_id)
141 revision=commit.raw_id)
142
142
143 # comments from PR
143 # comments from PR
144 statuses = ChangesetStatusModel().get_statuses(
144 statuses = ChangesetStatusModel().get_statuses(
145 self.db_repo.repo_id, commit.raw_id,
145 self.db_repo.repo_id, commit.raw_id,
146 with_revisions=True)
146 with_revisions=True)
147
147
148 prs = set()
148 prs = set()
149 reviewers = list()
149 reviewers = list()
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 for c_status in statuses:
151 for c_status in statuses:
152
152
153 # extract associated pull-requests from votes
153 # extract associated pull-requests from votes
154 if c_status.pull_request:
154 if c_status.pull_request:
155 prs.add(c_status.pull_request)
155 prs.add(c_status.pull_request)
156
156
157 # extract reviewers
157 # extract reviewers
158 _user_id = c_status.author.user_id
158 _user_id = c_status.author.user_id
159 if _user_id not in reviewers_duplicates:
159 if _user_id not in reviewers_duplicates:
160 reviewers.append(
160 reviewers.append(
161 StrictAttributeDict({
161 StrictAttributeDict({
162 'user': c_status.author,
162 'user': c_status.author,
163
163
164 # fake attributed for commit, page that we don't have
164 # fake attributed for commit, page that we don't have
165 # but we share the display with PR page
165 # but we share the display with PR page
166 'mandatory': False,
166 'mandatory': False,
167 'reasons': [],
167 'reasons': [],
168 'rule_user_group_data': lambda: None
168 'rule_user_group_data': lambda: None
169 })
169 })
170 )
170 )
171 reviewers_duplicates.add(_user_id)
171 reviewers_duplicates.add(_user_id)
172
172
173 c.allowed_reviewers = reviewers
173 c.allowed_reviewers = reviewers
174 # from associated statuses, check the pull requests, and
174 # from associated statuses, check the pull requests, and
175 # show comments from them
175 # show comments from them
176 for pr in prs:
176 for pr in prs:
177 c.comments.extend(pr.comments)
177 c.comments.extend(pr.comments)
178
178
179 c.unresolved_comments = CommentsModel()\
179 c.unresolved_comments = CommentsModel()\
180 .get_commit_unresolved_todos(commit.raw_id)
180 .get_commit_unresolved_todos(commit.raw_id)
181 c.resolved_comments = CommentsModel()\
181 c.resolved_comments = CommentsModel()\
182 .get_commit_resolved_todos(commit.raw_id)
182 .get_commit_resolved_todos(commit.raw_id)
183
183
184 c.inline_comments_flat = CommentsModel()\
184 c.inline_comments_flat = CommentsModel()\
185 .get_commit_inline_comments(commit.raw_id)
185 .get_commit_inline_comments(commit.raw_id)
186
186
187 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
187 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
188 statuses, reviewers)
188 statuses, reviewers)
189
189
190 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
190 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
191
191
192 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
192 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
193
193
194 for review_obj, member, reasons, mandatory, status in review_statuses:
194 for review_obj, member, reasons, mandatory, status in review_statuses:
195 member_reviewer = h.reviewer_as_json(
195 member_reviewer = h.reviewer_as_json(
196 member, reasons=reasons, mandatory=mandatory,
196 member, reasons=reasons, mandatory=mandatory, role=None,
197 user_group=None
197 user_group=None
198 )
198 )
199
199
200 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
200 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
201 member_reviewer['review_status'] = current_review_status
201 member_reviewer['review_status'] = current_review_status
202 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
202 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
203 member_reviewer['allowed_to_update'] = False
203 member_reviewer['allowed_to_update'] = False
204 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
204 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
205
205
206 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
206 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
207
207
208 # NOTE(marcink): this uses the same voting logic as in pull-requests
208 # NOTE(marcink): this uses the same voting logic as in pull-requests
209 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
209 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
210 c.commit_broadcast_channel = u'/repo${}$/commit/{}'.format(
210 c.commit_broadcast_channel = u'/repo${}$/commit/{}'.format(
211 c.repo_name,
211 c.repo_name,
212 commit.raw_id
212 commit.raw_id
213 )
213 )
214
214
215 diff = None
215 diff = None
216 # Iterate over ranges (default commit view is always one commit)
216 # Iterate over ranges (default commit view is always one commit)
217 for commit in c.commit_ranges:
217 for commit in c.commit_ranges:
218 c.changes[commit.raw_id] = []
218 c.changes[commit.raw_id] = []
219
219
220 commit2 = commit
220 commit2 = commit
221 commit1 = commit.first_parent
221 commit1 = commit.first_parent
222
222
223 if method == 'show':
223 if method == 'show':
224 inline_comments = CommentsModel().get_inline_comments(
224 inline_comments = CommentsModel().get_inline_comments(
225 self.db_repo.repo_id, revision=commit.raw_id)
225 self.db_repo.repo_id, revision=commit.raw_id)
226 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
226 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
227 inline_comments))
227 inline_comments))
228 c.inline_comments = inline_comments
228 c.inline_comments = inline_comments
229
229
230 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
230 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
231 self.db_repo)
231 self.db_repo)
232 cache_file_path = diff_cache_exist(
232 cache_file_path = diff_cache_exist(
233 cache_path, 'diff', commit.raw_id,
233 cache_path, 'diff', commit.raw_id,
234 hide_whitespace_changes, diff_context, c.fulldiff)
234 hide_whitespace_changes, diff_context, c.fulldiff)
235
235
236 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
236 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
237 force_recache = str2bool(self.request.GET.get('force_recache'))
237 force_recache = str2bool(self.request.GET.get('force_recache'))
238
238
239 cached_diff = None
239 cached_diff = None
240 if caching_enabled:
240 if caching_enabled:
241 cached_diff = load_cached_diff(cache_file_path)
241 cached_diff = load_cached_diff(cache_file_path)
242
242
243 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
243 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
244 if not force_recache and has_proper_diff_cache:
244 if not force_recache and has_proper_diff_cache:
245 diffset = cached_diff['diff']
245 diffset = cached_diff['diff']
246 else:
246 else:
247 vcs_diff = self.rhodecode_vcs_repo.get_diff(
247 vcs_diff = self.rhodecode_vcs_repo.get_diff(
248 commit1, commit2,
248 commit1, commit2,
249 ignore_whitespace=hide_whitespace_changes,
249 ignore_whitespace=hide_whitespace_changes,
250 context=diff_context)
250 context=diff_context)
251
251
252 diff_processor = diffs.DiffProcessor(
252 diff_processor = diffs.DiffProcessor(
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 file_limit=file_limit, show_full_diff=c.fulldiff)
254 file_limit=file_limit, show_full_diff=c.fulldiff)
255
255
256 _parsed = diff_processor.prepare()
256 _parsed = diff_processor.prepare()
257
257
258 diffset = codeblocks.DiffSet(
258 diffset = codeblocks.DiffSet(
259 repo_name=self.db_repo_name,
259 repo_name=self.db_repo_name,
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
262
262
263 diffset = self.path_filter.render_patchset_filtered(
263 diffset = self.path_filter.render_patchset_filtered(
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265
265
266 # save cached diff
266 # save cached diff
267 if caching_enabled:
267 if caching_enabled:
268 cache_diff(cache_file_path, diffset, None)
268 cache_diff(cache_file_path, diffset, None)
269
269
270 c.limited_diff = diffset.limited_diff
270 c.limited_diff = diffset.limited_diff
271 c.changes[commit.raw_id] = diffset
271 c.changes[commit.raw_id] = diffset
272 else:
272 else:
273 # TODO(marcink): no cache usage here...
273 # TODO(marcink): no cache usage here...
274 _diff = self.rhodecode_vcs_repo.get_diff(
274 _diff = self.rhodecode_vcs_repo.get_diff(
275 commit1, commit2,
275 commit1, commit2,
276 ignore_whitespace=hide_whitespace_changes, context=diff_context)
276 ignore_whitespace=hide_whitespace_changes, context=diff_context)
277 diff_processor = diffs.DiffProcessor(
277 diff_processor = diffs.DiffProcessor(
278 _diff, format='newdiff', diff_limit=diff_limit,
278 _diff, format='newdiff', diff_limit=diff_limit,
279 file_limit=file_limit, show_full_diff=c.fulldiff)
279 file_limit=file_limit, show_full_diff=c.fulldiff)
280 # downloads/raw we only need RAW diff nothing else
280 # downloads/raw we only need RAW diff nothing else
281 diff = self.path_filter.get_raw_patch(diff_processor)
281 diff = self.path_filter.get_raw_patch(diff_processor)
282 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
282 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
283
283
284 # sort comments by how they were generated
284 # sort comments by how they were generated
285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
286 c.at_version_num = None
286 c.at_version_num = None
287
287
288 if len(c.commit_ranges) == 1:
288 if len(c.commit_ranges) == 1:
289 c.commit = c.commit_ranges[0]
289 c.commit = c.commit_ranges[0]
290 c.parent_tmpl = ''.join(
290 c.parent_tmpl = ''.join(
291 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
291 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
292
292
293 if method == 'download':
293 if method == 'download':
294 response = Response(diff)
294 response = Response(diff)
295 response.content_type = 'text/plain'
295 response.content_type = 'text/plain'
296 response.content_disposition = (
296 response.content_disposition = (
297 'attachment; filename=%s.diff' % commit_id_range[:12])
297 'attachment; filename=%s.diff' % commit_id_range[:12])
298 return response
298 return response
299 elif method == 'patch':
299 elif method == 'patch':
300 c.diff = safe_unicode(diff)
300 c.diff = safe_unicode(diff)
301 patch = render(
301 patch = render(
302 'rhodecode:templates/changeset/patch_changeset.mako',
302 'rhodecode:templates/changeset/patch_changeset.mako',
303 self._get_template_context(c), self.request)
303 self._get_template_context(c), self.request)
304 response = Response(patch)
304 response = Response(patch)
305 response.content_type = 'text/plain'
305 response.content_type = 'text/plain'
306 return response
306 return response
307 elif method == 'raw':
307 elif method == 'raw':
308 response = Response(diff)
308 response = Response(diff)
309 response.content_type = 'text/plain'
309 response.content_type = 'text/plain'
310 return response
310 return response
311 elif method == 'show':
311 elif method == 'show':
312 if len(c.commit_ranges) == 1:
312 if len(c.commit_ranges) == 1:
313 html = render(
313 html = render(
314 'rhodecode:templates/changeset/changeset.mako',
314 'rhodecode:templates/changeset/changeset.mako',
315 self._get_template_context(c), self.request)
315 self._get_template_context(c), self.request)
316 return Response(html)
316 return Response(html)
317 else:
317 else:
318 c.ancestor = None
318 c.ancestor = None
319 c.target_repo = self.db_repo
319 c.target_repo = self.db_repo
320 html = render(
320 html = render(
321 'rhodecode:templates/changeset/changeset_range.mako',
321 'rhodecode:templates/changeset/changeset_range.mako',
322 self._get_template_context(c), self.request)
322 self._get_template_context(c), self.request)
323 return Response(html)
323 return Response(html)
324
324
325 raise HTTPBadRequest()
325 raise HTTPBadRequest()
326
326
327 @LoginRequired()
327 @LoginRequired()
328 @HasRepoPermissionAnyDecorator(
328 @HasRepoPermissionAnyDecorator(
329 'repository.read', 'repository.write', 'repository.admin')
329 'repository.read', 'repository.write', 'repository.admin')
330 @view_config(
330 @view_config(
331 route_name='repo_commit', request_method='GET',
331 route_name='repo_commit', request_method='GET',
332 renderer=None)
332 renderer=None)
333 def repo_commit_show(self):
333 def repo_commit_show(self):
334 commit_id = self.request.matchdict['commit_id']
334 commit_id = self.request.matchdict['commit_id']
335 return self._commit(commit_id, method='show')
335 return self._commit(commit_id, method='show')
336
336
337 @LoginRequired()
337 @LoginRequired()
338 @HasRepoPermissionAnyDecorator(
338 @HasRepoPermissionAnyDecorator(
339 'repository.read', 'repository.write', 'repository.admin')
339 'repository.read', 'repository.write', 'repository.admin')
340 @view_config(
340 @view_config(
341 route_name='repo_commit_raw', request_method='GET',
341 route_name='repo_commit_raw', request_method='GET',
342 renderer=None)
342 renderer=None)
343 @view_config(
343 @view_config(
344 route_name='repo_commit_raw_deprecated', request_method='GET',
344 route_name='repo_commit_raw_deprecated', request_method='GET',
345 renderer=None)
345 renderer=None)
346 def repo_commit_raw(self):
346 def repo_commit_raw(self):
347 commit_id = self.request.matchdict['commit_id']
347 commit_id = self.request.matchdict['commit_id']
348 return self._commit(commit_id, method='raw')
348 return self._commit(commit_id, method='raw')
349
349
350 @LoginRequired()
350 @LoginRequired()
351 @HasRepoPermissionAnyDecorator(
351 @HasRepoPermissionAnyDecorator(
352 'repository.read', 'repository.write', 'repository.admin')
352 'repository.read', 'repository.write', 'repository.admin')
353 @view_config(
353 @view_config(
354 route_name='repo_commit_patch', request_method='GET',
354 route_name='repo_commit_patch', request_method='GET',
355 renderer=None)
355 renderer=None)
356 def repo_commit_patch(self):
356 def repo_commit_patch(self):
357 commit_id = self.request.matchdict['commit_id']
357 commit_id = self.request.matchdict['commit_id']
358 return self._commit(commit_id, method='patch')
358 return self._commit(commit_id, method='patch')
359
359
360 @LoginRequired()
360 @LoginRequired()
361 @HasRepoPermissionAnyDecorator(
361 @HasRepoPermissionAnyDecorator(
362 'repository.read', 'repository.write', 'repository.admin')
362 'repository.read', 'repository.write', 'repository.admin')
363 @view_config(
363 @view_config(
364 route_name='repo_commit_download', request_method='GET',
364 route_name='repo_commit_download', request_method='GET',
365 renderer=None)
365 renderer=None)
366 def repo_commit_download(self):
366 def repo_commit_download(self):
367 commit_id = self.request.matchdict['commit_id']
367 commit_id = self.request.matchdict['commit_id']
368 return self._commit(commit_id, method='download')
368 return self._commit(commit_id, method='download')
369
369
370 @LoginRequired()
370 @LoginRequired()
371 @NotAnonymous()
371 @NotAnonymous()
372 @HasRepoPermissionAnyDecorator(
372 @HasRepoPermissionAnyDecorator(
373 'repository.read', 'repository.write', 'repository.admin')
373 'repository.read', 'repository.write', 'repository.admin')
374 @CSRFRequired()
374 @CSRFRequired()
375 @view_config(
375 @view_config(
376 route_name='repo_commit_comment_create', request_method='POST',
376 route_name='repo_commit_comment_create', request_method='POST',
377 renderer='json_ext')
377 renderer='json_ext')
378 def repo_commit_comment_create(self):
378 def repo_commit_comment_create(self):
379 _ = self.request.translate
379 _ = self.request.translate
380 commit_id = self.request.matchdict['commit_id']
380 commit_id = self.request.matchdict['commit_id']
381
381
382 c = self.load_default_context()
382 c = self.load_default_context()
383 status = self.request.POST.get('changeset_status', None)
383 status = self.request.POST.get('changeset_status', None)
384 text = self.request.POST.get('text')
384 text = self.request.POST.get('text')
385 comment_type = self.request.POST.get('comment_type')
385 comment_type = self.request.POST.get('comment_type')
386 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
387
387
388 if status:
388 if status:
389 text = text or (_('Status change %(transition_icon)s %(status)s')
389 text = text or (_('Status change %(transition_icon)s %(status)s')
390 % {'transition_icon': '>',
390 % {'transition_icon': '>',
391 'status': ChangesetStatus.get_status_lbl(status)})
391 'status': ChangesetStatus.get_status_lbl(status)})
392
392
393 multi_commit_ids = []
393 multi_commit_ids = []
394 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
394 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
395 if _commit_id not in ['', None, EmptyCommit.raw_id]:
395 if _commit_id not in ['', None, EmptyCommit.raw_id]:
396 if _commit_id not in multi_commit_ids:
396 if _commit_id not in multi_commit_ids:
397 multi_commit_ids.append(_commit_id)
397 multi_commit_ids.append(_commit_id)
398
398
399 commit_ids = multi_commit_ids or [commit_id]
399 commit_ids = multi_commit_ids or [commit_id]
400
400
401 comment = None
401 comment = None
402 for current_id in filter(None, commit_ids):
402 for current_id in filter(None, commit_ids):
403 comment = CommentsModel().create(
403 comment = CommentsModel().create(
404 text=text,
404 text=text,
405 repo=self.db_repo.repo_id,
405 repo=self.db_repo.repo_id,
406 user=self._rhodecode_db_user.user_id,
406 user=self._rhodecode_db_user.user_id,
407 commit_id=current_id,
407 commit_id=current_id,
408 f_path=self.request.POST.get('f_path'),
408 f_path=self.request.POST.get('f_path'),
409 line_no=self.request.POST.get('line'),
409 line_no=self.request.POST.get('line'),
410 status_change=(ChangesetStatus.get_status_lbl(status)
410 status_change=(ChangesetStatus.get_status_lbl(status)
411 if status else None),
411 if status else None),
412 status_change_type=status,
412 status_change_type=status,
413 comment_type=comment_type,
413 comment_type=comment_type,
414 resolves_comment_id=resolves_comment_id,
414 resolves_comment_id=resolves_comment_id,
415 auth_user=self._rhodecode_user
415 auth_user=self._rhodecode_user
416 )
416 )
417
417
418 # get status if set !
418 # get status if set !
419 if status:
419 if status:
420 # if latest status was from pull request and it's closed
420 # if latest status was from pull request and it's closed
421 # disallow changing status !
421 # disallow changing status !
422 # dont_allow_on_closed_pull_request = True !
422 # dont_allow_on_closed_pull_request = True !
423
423
424 try:
424 try:
425 ChangesetStatusModel().set_status(
425 ChangesetStatusModel().set_status(
426 self.db_repo.repo_id,
426 self.db_repo.repo_id,
427 status,
427 status,
428 self._rhodecode_db_user.user_id,
428 self._rhodecode_db_user.user_id,
429 comment,
429 comment,
430 revision=current_id,
430 revision=current_id,
431 dont_allow_on_closed_pull_request=True
431 dont_allow_on_closed_pull_request=True
432 )
432 )
433 except StatusChangeOnClosedPullRequestError:
433 except StatusChangeOnClosedPullRequestError:
434 msg = _('Changing the status of a commit associated with '
434 msg = _('Changing the status of a commit associated with '
435 'a closed pull request is not allowed')
435 'a closed pull request is not allowed')
436 log.exception(msg)
436 log.exception(msg)
437 h.flash(msg, category='warning')
437 h.flash(msg, category='warning')
438 raise HTTPFound(h.route_path(
438 raise HTTPFound(h.route_path(
439 'repo_commit', repo_name=self.db_repo_name,
439 'repo_commit', repo_name=self.db_repo_name,
440 commit_id=current_id))
440 commit_id=current_id))
441
441
442 commit = self.db_repo.get_commit(current_id)
442 commit = self.db_repo.get_commit(current_id)
443 CommentsModel().trigger_commit_comment_hook(
443 CommentsModel().trigger_commit_comment_hook(
444 self.db_repo, self._rhodecode_user, 'create',
444 self.db_repo, self._rhodecode_user, 'create',
445 data={'comment': comment, 'commit': commit})
445 data={'comment': comment, 'commit': commit})
446
446
447 # finalize, commit and redirect
447 # finalize, commit and redirect
448 Session().commit()
448 Session().commit()
449
449
450 data = {
450 data = {
451 'target_id': h.safeid(h.safe_unicode(
451 'target_id': h.safeid(h.safe_unicode(
452 self.request.POST.get('f_path'))),
452 self.request.POST.get('f_path'))),
453 }
453 }
454 if comment:
454 if comment:
455 c.co = comment
455 c.co = comment
456 c.at_version_num = 0
456 c.at_version_num = 0
457 rendered_comment = render(
457 rendered_comment = render(
458 'rhodecode:templates/changeset/changeset_comment_block.mako',
458 'rhodecode:templates/changeset/changeset_comment_block.mako',
459 self._get_template_context(c), self.request)
459 self._get_template_context(c), self.request)
460
460
461 data.update(comment.get_dict())
461 data.update(comment.get_dict())
462 data.update({'rendered_text': rendered_comment})
462 data.update({'rendered_text': rendered_comment})
463
463
464 return data
464 return data
465
465
466 @LoginRequired()
466 @LoginRequired()
467 @NotAnonymous()
467 @NotAnonymous()
468 @HasRepoPermissionAnyDecorator(
468 @HasRepoPermissionAnyDecorator(
469 'repository.read', 'repository.write', 'repository.admin')
469 'repository.read', 'repository.write', 'repository.admin')
470 @CSRFRequired()
470 @CSRFRequired()
471 @view_config(
471 @view_config(
472 route_name='repo_commit_comment_preview', request_method='POST',
472 route_name='repo_commit_comment_preview', request_method='POST',
473 renderer='string', xhr=True)
473 renderer='string', xhr=True)
474 def repo_commit_comment_preview(self):
474 def repo_commit_comment_preview(self):
475 # Technically a CSRF token is not needed as no state changes with this
475 # Technically a CSRF token is not needed as no state changes with this
476 # call. However, as this is a POST is better to have it, so automated
476 # call. However, as this is a POST is better to have it, so automated
477 # tools don't flag it as potential CSRF.
477 # tools don't flag it as potential CSRF.
478 # Post is required because the payload could be bigger than the maximum
478 # Post is required because the payload could be bigger than the maximum
479 # allowed by GET.
479 # allowed by GET.
480
480
481 text = self.request.POST.get('text')
481 text = self.request.POST.get('text')
482 renderer = self.request.POST.get('renderer') or 'rst'
482 renderer = self.request.POST.get('renderer') or 'rst'
483 if text:
483 if text:
484 return h.render(text, renderer=renderer, mentions=True,
484 return h.render(text, renderer=renderer, mentions=True,
485 repo_name=self.db_repo_name)
485 repo_name=self.db_repo_name)
486 return ''
486 return ''
487
487
488 @LoginRequired()
488 @LoginRequired()
489 @HasRepoPermissionAnyDecorator(
489 @HasRepoPermissionAnyDecorator(
490 'repository.read', 'repository.write', 'repository.admin')
490 'repository.read', 'repository.write', 'repository.admin')
491 @CSRFRequired()
491 @CSRFRequired()
492 @view_config(
492 @view_config(
493 route_name='repo_commit_comment_history_view', request_method='POST',
493 route_name='repo_commit_comment_history_view', request_method='POST',
494 renderer='string', xhr=True)
494 renderer='string', xhr=True)
495 def repo_commit_comment_history_view(self):
495 def repo_commit_comment_history_view(self):
496 c = self.load_default_context()
496 c = self.load_default_context()
497
497
498 comment_history_id = self.request.matchdict['comment_history_id']
498 comment_history_id = self.request.matchdict['comment_history_id']
499 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
499 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
500 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
500 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
501
501
502 if is_repo_comment:
502 if is_repo_comment:
503 c.comment_history = comment_history
503 c.comment_history = comment_history
504
504
505 rendered_comment = render(
505 rendered_comment = render(
506 'rhodecode:templates/changeset/comment_history.mako',
506 'rhodecode:templates/changeset/comment_history.mako',
507 self._get_template_context(c)
507 self._get_template_context(c)
508 , self.request)
508 , self.request)
509 return rendered_comment
509 return rendered_comment
510 else:
510 else:
511 log.warning('No permissions for user %s to show comment_history_id: %s',
511 log.warning('No permissions for user %s to show comment_history_id: %s',
512 self._rhodecode_db_user, comment_history_id)
512 self._rhodecode_db_user, comment_history_id)
513 raise HTTPNotFound()
513 raise HTTPNotFound()
514
514
515 @LoginRequired()
515 @LoginRequired()
516 @NotAnonymous()
516 @NotAnonymous()
517 @HasRepoPermissionAnyDecorator(
517 @HasRepoPermissionAnyDecorator(
518 'repository.read', 'repository.write', 'repository.admin')
518 'repository.read', 'repository.write', 'repository.admin')
519 @CSRFRequired()
519 @CSRFRequired()
520 @view_config(
520 @view_config(
521 route_name='repo_commit_comment_attachment_upload', request_method='POST',
521 route_name='repo_commit_comment_attachment_upload', request_method='POST',
522 renderer='json_ext', xhr=True)
522 renderer='json_ext', xhr=True)
523 def repo_commit_comment_attachment_upload(self):
523 def repo_commit_comment_attachment_upload(self):
524 c = self.load_default_context()
524 c = self.load_default_context()
525 upload_key = 'attachment'
525 upload_key = 'attachment'
526
526
527 file_obj = self.request.POST.get(upload_key)
527 file_obj = self.request.POST.get(upload_key)
528
528
529 if file_obj is None:
529 if file_obj is None:
530 self.request.response.status = 400
530 self.request.response.status = 400
531 return {'store_fid': None,
531 return {'store_fid': None,
532 'access_path': None,
532 'access_path': None,
533 'error': '{} data field is missing'.format(upload_key)}
533 'error': '{} data field is missing'.format(upload_key)}
534
534
535 if not hasattr(file_obj, 'filename'):
535 if not hasattr(file_obj, 'filename'):
536 self.request.response.status = 400
536 self.request.response.status = 400
537 return {'store_fid': None,
537 return {'store_fid': None,
538 'access_path': None,
538 'access_path': None,
539 'error': 'filename cannot be read from the data field'}
539 'error': 'filename cannot be read from the data field'}
540
540
541 filename = file_obj.filename
541 filename = file_obj.filename
542 file_display_name = filename
542 file_display_name = filename
543
543
544 metadata = {
544 metadata = {
545 'user_uploaded': {'username': self._rhodecode_user.username,
545 'user_uploaded': {'username': self._rhodecode_user.username,
546 'user_id': self._rhodecode_user.user_id,
546 'user_id': self._rhodecode_user.user_id,
547 'ip': self._rhodecode_user.ip_addr}}
547 'ip': self._rhodecode_user.ip_addr}}
548
548
549 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
549 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
550 allowed_extensions = [
550 allowed_extensions = [
551 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
551 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
552 '.pptx', '.txt', '.xlsx', '.zip']
552 '.pptx', '.txt', '.xlsx', '.zip']
553 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
553 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
554
554
555 try:
555 try:
556 storage = store_utils.get_file_storage(self.request.registry.settings)
556 storage = store_utils.get_file_storage(self.request.registry.settings)
557 store_uid, metadata = storage.save_file(
557 store_uid, metadata = storage.save_file(
558 file_obj.file, filename, extra_metadata=metadata,
558 file_obj.file, filename, extra_metadata=metadata,
559 extensions=allowed_extensions, max_filesize=max_file_size)
559 extensions=allowed_extensions, max_filesize=max_file_size)
560 except FileNotAllowedException:
560 except FileNotAllowedException:
561 self.request.response.status = 400
561 self.request.response.status = 400
562 permitted_extensions = ', '.join(allowed_extensions)
562 permitted_extensions = ', '.join(allowed_extensions)
563 error_msg = 'File `{}` is not allowed. ' \
563 error_msg = 'File `{}` is not allowed. ' \
564 'Only following extensions are permitted: {}'.format(
564 'Only following extensions are permitted: {}'.format(
565 filename, permitted_extensions)
565 filename, permitted_extensions)
566 return {'store_fid': None,
566 return {'store_fid': None,
567 'access_path': None,
567 'access_path': None,
568 'error': error_msg}
568 'error': error_msg}
569 except FileOverSizeException:
569 except FileOverSizeException:
570 self.request.response.status = 400
570 self.request.response.status = 400
571 limit_mb = h.format_byte_size_binary(max_file_size)
571 limit_mb = h.format_byte_size_binary(max_file_size)
572 return {'store_fid': None,
572 return {'store_fid': None,
573 'access_path': None,
573 'access_path': None,
574 'error': 'File {} is exceeding allowed limit of {}.'.format(
574 'error': 'File {} is exceeding allowed limit of {}.'.format(
575 filename, limit_mb)}
575 filename, limit_mb)}
576
576
577 try:
577 try:
578 entry = FileStore.create(
578 entry = FileStore.create(
579 file_uid=store_uid, filename=metadata["filename"],
579 file_uid=store_uid, filename=metadata["filename"],
580 file_hash=metadata["sha256"], file_size=metadata["size"],
580 file_hash=metadata["sha256"], file_size=metadata["size"],
581 file_display_name=file_display_name,
581 file_display_name=file_display_name,
582 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
582 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
583 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
583 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
584 scope_repo_id=self.db_repo.repo_id
584 scope_repo_id=self.db_repo.repo_id
585 )
585 )
586 Session().add(entry)
586 Session().add(entry)
587 Session().commit()
587 Session().commit()
588 log.debug('Stored upload in DB as %s', entry)
588 log.debug('Stored upload in DB as %s', entry)
589 except Exception:
589 except Exception:
590 log.exception('Failed to store file %s', filename)
590 log.exception('Failed to store file %s', filename)
591 self.request.response.status = 400
591 self.request.response.status = 400
592 return {'store_fid': None,
592 return {'store_fid': None,
593 'access_path': None,
593 'access_path': None,
594 'error': 'File {} failed to store in DB.'.format(filename)}
594 'error': 'File {} failed to store in DB.'.format(filename)}
595
595
596 Session().commit()
596 Session().commit()
597
597
598 return {
598 return {
599 'store_fid': store_uid,
599 'store_fid': store_uid,
600 'access_path': h.route_path(
600 'access_path': h.route_path(
601 'download_file', fid=store_uid),
601 'download_file', fid=store_uid),
602 'fqn_access_path': h.route_url(
602 'fqn_access_path': h.route_url(
603 'download_file', fid=store_uid),
603 'download_file', fid=store_uid),
604 'repo_access_path': h.route_path(
604 'repo_access_path': h.route_path(
605 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
605 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
606 'repo_fqn_access_path': h.route_url(
606 'repo_fqn_access_path': h.route_url(
607 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
607 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
608 }
608 }
609
609
610 @LoginRequired()
610 @LoginRequired()
611 @NotAnonymous()
611 @NotAnonymous()
612 @HasRepoPermissionAnyDecorator(
612 @HasRepoPermissionAnyDecorator(
613 'repository.read', 'repository.write', 'repository.admin')
613 'repository.read', 'repository.write', 'repository.admin')
614 @CSRFRequired()
614 @CSRFRequired()
615 @view_config(
615 @view_config(
616 route_name='repo_commit_comment_delete', request_method='POST',
616 route_name='repo_commit_comment_delete', request_method='POST',
617 renderer='json_ext')
617 renderer='json_ext')
618 def repo_commit_comment_delete(self):
618 def repo_commit_comment_delete(self):
619 commit_id = self.request.matchdict['commit_id']
619 commit_id = self.request.matchdict['commit_id']
620 comment_id = self.request.matchdict['comment_id']
620 comment_id = self.request.matchdict['comment_id']
621
621
622 comment = ChangesetComment.get_or_404(comment_id)
622 comment = ChangesetComment.get_or_404(comment_id)
623 if not comment:
623 if not comment:
624 log.debug('Comment with id:%s not found, skipping', comment_id)
624 log.debug('Comment with id:%s not found, skipping', comment_id)
625 # comment already deleted in another call probably
625 # comment already deleted in another call probably
626 return True
626 return True
627
627
628 if comment.immutable:
628 if comment.immutable:
629 # don't allow deleting comments that are immutable
629 # don't allow deleting comments that are immutable
630 raise HTTPForbidden()
630 raise HTTPForbidden()
631
631
632 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
632 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
633 super_admin = h.HasPermissionAny('hg.admin')()
633 super_admin = h.HasPermissionAny('hg.admin')()
634 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
634 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
635 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
635 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
636 comment_repo_admin = is_repo_admin and is_repo_comment
636 comment_repo_admin = is_repo_admin and is_repo_comment
637
637
638 if super_admin or comment_owner or comment_repo_admin:
638 if super_admin or comment_owner or comment_repo_admin:
639 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
639 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
640 Session().commit()
640 Session().commit()
641 return True
641 return True
642 else:
642 else:
643 log.warning('No permissions for user %s to delete comment_id: %s',
643 log.warning('No permissions for user %s to delete comment_id: %s',
644 self._rhodecode_db_user, comment_id)
644 self._rhodecode_db_user, comment_id)
645 raise HTTPNotFound()
645 raise HTTPNotFound()
646
646
647 @LoginRequired()
647 @LoginRequired()
648 @NotAnonymous()
648 @NotAnonymous()
649 @HasRepoPermissionAnyDecorator(
649 @HasRepoPermissionAnyDecorator(
650 'repository.read', 'repository.write', 'repository.admin')
650 'repository.read', 'repository.write', 'repository.admin')
651 @CSRFRequired()
651 @CSRFRequired()
652 @view_config(
652 @view_config(
653 route_name='repo_commit_comment_edit', request_method='POST',
653 route_name='repo_commit_comment_edit', request_method='POST',
654 renderer='json_ext')
654 renderer='json_ext')
655 def repo_commit_comment_edit(self):
655 def repo_commit_comment_edit(self):
656 self.load_default_context()
656 self.load_default_context()
657
657
658 comment_id = self.request.matchdict['comment_id']
658 comment_id = self.request.matchdict['comment_id']
659 comment = ChangesetComment.get_or_404(comment_id)
659 comment = ChangesetComment.get_or_404(comment_id)
660
660
661 if comment.immutable:
661 if comment.immutable:
662 # don't allow deleting comments that are immutable
662 # don't allow deleting comments that are immutable
663 raise HTTPForbidden()
663 raise HTTPForbidden()
664
664
665 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
665 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
666 super_admin = h.HasPermissionAny('hg.admin')()
666 super_admin = h.HasPermissionAny('hg.admin')()
667 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
667 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
668 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
668 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
669 comment_repo_admin = is_repo_admin and is_repo_comment
669 comment_repo_admin = is_repo_admin and is_repo_comment
670
670
671 if super_admin or comment_owner or comment_repo_admin:
671 if super_admin or comment_owner or comment_repo_admin:
672 text = self.request.POST.get('text')
672 text = self.request.POST.get('text')
673 version = self.request.POST.get('version')
673 version = self.request.POST.get('version')
674 if text == comment.text:
674 if text == comment.text:
675 log.warning(
675 log.warning(
676 'Comment(repo): '
676 'Comment(repo): '
677 'Trying to create new version '
677 'Trying to create new version '
678 'with the same comment body {}'.format(
678 'with the same comment body {}'.format(
679 comment_id,
679 comment_id,
680 )
680 )
681 )
681 )
682 raise HTTPNotFound()
682 raise HTTPNotFound()
683
683
684 if version.isdigit():
684 if version.isdigit():
685 version = int(version)
685 version = int(version)
686 else:
686 else:
687 log.warning(
687 log.warning(
688 'Comment(repo): Wrong version type {} {} '
688 'Comment(repo): Wrong version type {} {} '
689 'for comment {}'.format(
689 'for comment {}'.format(
690 version,
690 version,
691 type(version),
691 type(version),
692 comment_id,
692 comment_id,
693 )
693 )
694 )
694 )
695 raise HTTPNotFound()
695 raise HTTPNotFound()
696
696
697 try:
697 try:
698 comment_history = CommentsModel().edit(
698 comment_history = CommentsModel().edit(
699 comment_id=comment_id,
699 comment_id=comment_id,
700 text=text,
700 text=text,
701 auth_user=self._rhodecode_user,
701 auth_user=self._rhodecode_user,
702 version=version,
702 version=version,
703 )
703 )
704 except CommentVersionMismatch:
704 except CommentVersionMismatch:
705 raise HTTPConflict()
705 raise HTTPConflict()
706
706
707 if not comment_history:
707 if not comment_history:
708 raise HTTPNotFound()
708 raise HTTPNotFound()
709
709
710 commit_id = self.request.matchdict['commit_id']
710 commit_id = self.request.matchdict['commit_id']
711 commit = self.db_repo.get_commit(commit_id)
711 commit = self.db_repo.get_commit(commit_id)
712 CommentsModel().trigger_commit_comment_hook(
712 CommentsModel().trigger_commit_comment_hook(
713 self.db_repo, self._rhodecode_user, 'edit',
713 self.db_repo, self._rhodecode_user, 'edit',
714 data={'comment': comment, 'commit': commit})
714 data={'comment': comment, 'commit': commit})
715
715
716 Session().commit()
716 Session().commit()
717 return {
717 return {
718 'comment_history_id': comment_history.comment_history_id,
718 'comment_history_id': comment_history.comment_history_id,
719 'comment_id': comment.comment_id,
719 'comment_id': comment.comment_id,
720 'comment_version': comment_history.version,
720 'comment_version': comment_history.version,
721 'comment_author_username': comment_history.author.username,
721 'comment_author_username': comment_history.author.username,
722 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
722 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
723 'comment_created_on': h.age_component(comment_history.created_on,
723 'comment_created_on': h.age_component(comment_history.created_on,
724 time_is_local=True),
724 time_is_local=True),
725 }
725 }
726 else:
726 else:
727 log.warning('No permissions for user %s to edit comment_id: %s',
727 log.warning('No permissions for user %s to edit comment_id: %s',
728 self._rhodecode_db_user, comment_id)
728 self._rhodecode_db_user, comment_id)
729 raise HTTPNotFound()
729 raise HTTPNotFound()
730
730
731 @LoginRequired()
731 @LoginRequired()
732 @HasRepoPermissionAnyDecorator(
732 @HasRepoPermissionAnyDecorator(
733 'repository.read', 'repository.write', 'repository.admin')
733 'repository.read', 'repository.write', 'repository.admin')
734 @view_config(
734 @view_config(
735 route_name='repo_commit_data', request_method='GET',
735 route_name='repo_commit_data', request_method='GET',
736 renderer='json_ext', xhr=True)
736 renderer='json_ext', xhr=True)
737 def repo_commit_data(self):
737 def repo_commit_data(self):
738 commit_id = self.request.matchdict['commit_id']
738 commit_id = self.request.matchdict['commit_id']
739 self.load_default_context()
739 self.load_default_context()
740
740
741 try:
741 try:
742 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
742 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
743 except CommitDoesNotExistError as e:
743 except CommitDoesNotExistError as e:
744 return EmptyCommit(message=str(e))
744 return EmptyCommit(message=str(e))
745
745
746 @LoginRequired()
746 @LoginRequired()
747 @HasRepoPermissionAnyDecorator(
747 @HasRepoPermissionAnyDecorator(
748 'repository.read', 'repository.write', 'repository.admin')
748 'repository.read', 'repository.write', 'repository.admin')
749 @view_config(
749 @view_config(
750 route_name='repo_commit_children', request_method='GET',
750 route_name='repo_commit_children', request_method='GET',
751 renderer='json_ext', xhr=True)
751 renderer='json_ext', xhr=True)
752 def repo_commit_children(self):
752 def repo_commit_children(self):
753 commit_id = self.request.matchdict['commit_id']
753 commit_id = self.request.matchdict['commit_id']
754 self.load_default_context()
754 self.load_default_context()
755
755
756 try:
756 try:
757 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
757 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
758 children = commit.children
758 children = commit.children
759 except CommitDoesNotExistError:
759 except CommitDoesNotExistError:
760 children = []
760 children = []
761
761
762 result = {"results": children}
762 result = {"results": children}
763 return result
763 return result
764
764
765 @LoginRequired()
765 @LoginRequired()
766 @HasRepoPermissionAnyDecorator(
766 @HasRepoPermissionAnyDecorator(
767 'repository.read', 'repository.write', 'repository.admin')
767 'repository.read', 'repository.write', 'repository.admin')
768 @view_config(
768 @view_config(
769 route_name='repo_commit_parents', request_method='GET',
769 route_name='repo_commit_parents', request_method='GET',
770 renderer='json_ext')
770 renderer='json_ext')
771 def repo_commit_parents(self):
771 def repo_commit_parents(self):
772 commit_id = self.request.matchdict['commit_id']
772 commit_id = self.request.matchdict['commit_id']
773 self.load_default_context()
773 self.load_default_context()
774
774
775 try:
775 try:
776 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
776 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
777 parents = commit.parents
777 parents = commit.parents
778 except CommitDoesNotExistError:
778 except CommitDoesNotExistError:
779 parents = []
779 parents = []
780 result = {"results": parents}
780 result = {"results": parents}
781 return result
781 return result
@@ -1,1757 +1,1794 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
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 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 PullRequestReviewers)
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.scm import ScmModel
54
55
55 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
56
57
57
58
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
60
60 def load_default_context(self):
61 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 # backward compat., we use for OLD PRs a plain renderer
65 # backward compat., we use for OLD PRs a plain renderer
65 c.renderer = 'plain'
66 c.renderer = 'plain'
66 return c
67 return c
67
68
68 def _get_pull_requests_list(
69 def _get_pull_requests_list(
69 self, repo_name, source, filter_type, opened_by, statuses):
70 self, repo_name, source, filter_type, opened_by, statuses):
70
71
71 draw, start, limit = self._extract_chunk(self.request)
72 draw, start, limit = self._extract_chunk(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 _render = self.request.get_partial_renderer(
74 _render = self.request.get_partial_renderer(
74 'rhodecode:templates/data_table/_dt_elements.mako')
75 'rhodecode:templates/data_table/_dt_elements.mako')
75
76
76 # pagination
77 # pagination
77
78
78 if filter_type == 'awaiting_review':
79 if filter_type == 'awaiting_review':
79 pull_requests = PullRequestModel().get_awaiting_review(
80 pull_requests = PullRequestModel().get_awaiting_review(
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 statuses=statuses, offset=start, length=limit,
82 statuses=statuses, offset=start, length=limit,
82 order_by=order_by, order_dir=order_dir)
83 order_by=order_by, order_dir=order_dir)
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 repo_name, search_q=search_q, source=source, statuses=statuses,
85 repo_name, search_q=search_q, source=source, statuses=statuses,
85 opened_by=opened_by)
86 opened_by=opened_by)
86 elif filter_type == 'awaiting_my_review':
87 elif filter_type == 'awaiting_my_review':
87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 pull_requests = PullRequestModel().get_awaiting_my_review(
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 offset=start, length=limit, order_by=order_by,
91 offset=start, length=limit, order_by=order_by,
91 order_dir=order_dir)
92 order_dir=order_dir)
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 statuses=statuses, opened_by=opened_by)
95 statuses=statuses, opened_by=opened_by)
95 else:
96 else:
96 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 statuses=statuses, offset=start, length=limit,
99 statuses=statuses, offset=start, length=limit,
99 order_by=order_by, order_dir=order_dir)
100 order_by=order_by, order_dir=order_dir)
100 pull_requests_total_count = PullRequestModel().count_all(
101 pull_requests_total_count = PullRequestModel().count_all(
101 repo_name, search_q=search_q, source=source, statuses=statuses,
102 repo_name, search_q=search_q, source=source, statuses=statuses,
102 opened_by=opened_by)
103 opened_by=opened_by)
103
104
104 data = []
105 data = []
105 comments_model = CommentsModel()
106 comments_model = CommentsModel()
106 for pr in pull_requests:
107 for pr in pull_requests:
107 comments = comments_model.get_all_comments(
108 comments = comments_model.get_all_comments(
108 self.db_repo.repo_id, pull_request=pr)
109 self.db_repo.repo_id, pull_request=pr)
109
110
110 data.append({
111 data.append({
111 'name': _render('pullrequest_name',
112 'name': _render('pullrequest_name',
112 pr.pull_request_id, pr.pull_request_state,
113 pr.pull_request_id, pr.pull_request_state,
113 pr.work_in_progress, pr.target_repo.repo_name),
114 pr.work_in_progress, pr.target_repo.repo_name),
114 'name_raw': pr.pull_request_id,
115 'name_raw': pr.pull_request_id,
115 'status': _render('pullrequest_status',
116 'status': _render('pullrequest_status',
116 pr.calculated_review_status()),
117 pr.calculated_review_status()),
117 'title': _render('pullrequest_title', pr.title, pr.description),
118 'title': _render('pullrequest_title', pr.title, pr.description),
118 'description': h.escape(pr.description),
119 'description': h.escape(pr.description),
119 'updated_on': _render('pullrequest_updated_on',
120 'updated_on': _render('pullrequest_updated_on',
120 h.datetime_to_time(pr.updated_on)),
121 h.datetime_to_time(pr.updated_on)),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'created_on': _render('pullrequest_updated_on',
123 'created_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.created_on)),
124 h.datetime_to_time(pr.created_on)),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'state': pr.pull_request_state,
126 'state': pr.pull_request_state,
126 'author': _render('pullrequest_author',
127 'author': _render('pullrequest_author',
127 pr.author.full_contact, ),
128 pr.author.full_contact, ),
128 'author_raw': pr.author.full_name,
129 'author_raw': pr.author.full_name,
129 'comments': _render('pullrequest_comments', len(comments)),
130 'comments': _render('pullrequest_comments', len(comments)),
130 'comments_raw': len(comments),
131 'comments_raw': len(comments),
131 'closed': pr.is_closed(),
132 'closed': pr.is_closed(),
132 })
133 })
133
134
134 data = ({
135 data = ({
135 'draw': draw,
136 'draw': draw,
136 'data': data,
137 'data': data,
137 'recordsTotal': pull_requests_total_count,
138 'recordsTotal': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
139 'recordsFiltered': pull_requests_total_count,
139 })
140 })
140 return data
141 return data
141
142
142 @LoginRequired()
143 @LoginRequired()
143 @HasRepoPermissionAnyDecorator(
144 @HasRepoPermissionAnyDecorator(
144 'repository.read', 'repository.write', 'repository.admin')
145 'repository.read', 'repository.write', 'repository.admin')
145 @view_config(
146 @view_config(
146 route_name='pullrequest_show_all', request_method='GET',
147 route_name='pullrequest_show_all', request_method='GET',
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 def pull_request_list(self):
149 def pull_request_list(self):
149 c = self.load_default_context()
150 c = self.load_default_context()
150
151
151 req_get = self.request.GET
152 req_get = self.request.GET
152 c.source = str2bool(req_get.get('source'))
153 c.source = str2bool(req_get.get('source'))
153 c.closed = str2bool(req_get.get('closed'))
154 c.closed = str2bool(req_get.get('closed'))
154 c.my = str2bool(req_get.get('my'))
155 c.my = str2bool(req_get.get('my'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157
158
158 c.active = 'open'
159 c.active = 'open'
159 if c.my:
160 if c.my:
160 c.active = 'my'
161 c.active = 'my'
161 if c.closed:
162 if c.closed:
162 c.active = 'closed'
163 c.active = 'closed'
163 if c.awaiting_review and not c.source:
164 if c.awaiting_review and not c.source:
164 c.active = 'awaiting'
165 c.active = 'awaiting'
165 if c.source and not c.awaiting_review:
166 if c.source and not c.awaiting_review:
166 c.active = 'source'
167 c.active = 'source'
167 if c.awaiting_my_review:
168 if c.awaiting_my_review:
168 c.active = 'awaiting_my'
169 c.active = 'awaiting_my'
169
170
170 return self._get_template_context(c)
171 return self._get_template_context(c)
171
172
172 @LoginRequired()
173 @LoginRequired()
173 @HasRepoPermissionAnyDecorator(
174 @HasRepoPermissionAnyDecorator(
174 'repository.read', 'repository.write', 'repository.admin')
175 'repository.read', 'repository.write', 'repository.admin')
175 @view_config(
176 @view_config(
176 route_name='pullrequest_show_all_data', request_method='GET',
177 route_name='pullrequest_show_all_data', request_method='GET',
177 renderer='json_ext', xhr=True)
178 renderer='json_ext', xhr=True)
178 def pull_request_list_data(self):
179 def pull_request_list_data(self):
179 self.load_default_context()
180 self.load_default_context()
180
181
181 # additional filters
182 # additional filters
182 req_get = self.request.GET
183 req_get = self.request.GET
183 source = str2bool(req_get.get('source'))
184 source = str2bool(req_get.get('source'))
184 closed = str2bool(req_get.get('closed'))
185 closed = str2bool(req_get.get('closed'))
185 my = str2bool(req_get.get('my'))
186 my = str2bool(req_get.get('my'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188
189
189 filter_type = 'awaiting_review' if awaiting_review \
190 filter_type = 'awaiting_review' if awaiting_review \
190 else 'awaiting_my_review' if awaiting_my_review \
191 else 'awaiting_my_review' if awaiting_my_review \
191 else None
192 else None
192
193
193 opened_by = None
194 opened_by = None
194 if my:
195 if my:
195 opened_by = [self._rhodecode_user.user_id]
196 opened_by = [self._rhodecode_user.user_id]
196
197
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 if closed:
199 if closed:
199 statuses = [PullRequest.STATUS_CLOSED]
200 statuses = [PullRequest.STATUS_CLOSED]
200
201
201 data = self._get_pull_requests_list(
202 data = self._get_pull_requests_list(
202 repo_name=self.db_repo_name, source=source,
203 repo_name=self.db_repo_name, source=source,
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204
205
205 return data
206 return data
206
207
207 def _is_diff_cache_enabled(self, target_repo):
208 def _is_diff_cache_enabled(self, target_repo):
208 caching_enabled = self._get_general_setting(
209 caching_enabled = self._get_general_setting(
209 target_repo, 'rhodecode_diff_cache')
210 target_repo, 'rhodecode_diff_cache')
210 log.debug('Diff caching enabled: %s', caching_enabled)
211 log.debug('Diff caching enabled: %s', caching_enabled)
211 return caching_enabled
212 return caching_enabled
212
213
213 def _get_diffset(self, source_repo_name, source_repo,
214 def _get_diffset(self, source_repo_name, source_repo,
214 ancestor_commit,
215 ancestor_commit,
215 source_ref_id, target_ref_id,
216 source_ref_id, target_ref_id,
216 target_commit, source_commit, diff_limit, file_limit,
217 target_commit, source_commit, diff_limit, file_limit,
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218
219
219 if use_ancestor:
220 if use_ancestor:
220 # we might want to not use it for versions
221 # we might want to not use it for versions
221 target_ref_id = ancestor_commit.raw_id
222 target_ref_id = ancestor_commit.raw_id
222
223
223 vcs_diff = PullRequestModel().get_diff(
224 vcs_diff = PullRequestModel().get_diff(
224 source_repo, source_ref_id, target_ref_id,
225 source_repo, source_ref_id, target_ref_id,
225 hide_whitespace_changes, diff_context)
226 hide_whitespace_changes, diff_context)
226
227
227 diff_processor = diffs.DiffProcessor(
228 diff_processor = diffs.DiffProcessor(
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 file_limit=file_limit, show_full_diff=fulldiff)
230 file_limit=file_limit, show_full_diff=fulldiff)
230
231
231 _parsed = diff_processor.prepare()
232 _parsed = diff_processor.prepare()
232
233
233 diffset = codeblocks.DiffSet(
234 diffset = codeblocks.DiffSet(
234 repo_name=self.db_repo_name,
235 repo_name=self.db_repo_name,
235 source_repo_name=source_repo_name,
236 source_repo_name=source_repo_name,
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 )
239 )
239 diffset = self.path_filter.render_patchset_filtered(
240 diffset = self.path_filter.render_patchset_filtered(
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241
242
242 return diffset
243 return diffset
243
244
244 def _get_range_diffset(self, source_scm, source_repo,
245 def _get_range_diffset(self, source_scm, source_repo,
245 commit1, commit2, diff_limit, file_limit,
246 commit1, commit2, diff_limit, file_limit,
246 fulldiff, hide_whitespace_changes, diff_context):
247 fulldiff, hide_whitespace_changes, diff_context):
247 vcs_diff = source_scm.get_diff(
248 vcs_diff = source_scm.get_diff(
248 commit1, commit2,
249 commit1, commit2,
249 ignore_whitespace=hide_whitespace_changes,
250 ignore_whitespace=hide_whitespace_changes,
250 context=diff_context)
251 context=diff_context)
251
252
252 diff_processor = diffs.DiffProcessor(
253 diff_processor = diffs.DiffProcessor(
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 file_limit=file_limit, show_full_diff=fulldiff)
255 file_limit=file_limit, show_full_diff=fulldiff)
255
256
256 _parsed = diff_processor.prepare()
257 _parsed = diff_processor.prepare()
257
258
258 diffset = codeblocks.DiffSet(
259 diffset = codeblocks.DiffSet(
259 repo_name=source_repo.repo_name,
260 repo_name=source_repo.repo_name,
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
262 target_node_getter=codeblocks.diffset_node_getter(commit2))
262
263
263 diffset = self.path_filter.render_patchset_filtered(
264 diffset = self.path_filter.render_patchset_filtered(
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265
266
266 return diffset
267 return diffset
267
268
268 def register_comments_vars(self, c, pull_request, versions):
269 def register_comments_vars(self, c, pull_request, versions):
269 comments_model = CommentsModel()
270 comments_model = CommentsModel()
270
271
271 # GENERAL COMMENTS with versions #
272 # GENERAL COMMENTS with versions #
272 q = comments_model._all_general_comments_of_pull_request(pull_request)
273 q = comments_model._all_general_comments_of_pull_request(pull_request)
273 q = q.order_by(ChangesetComment.comment_id.asc())
274 q = q.order_by(ChangesetComment.comment_id.asc())
274 general_comments = q
275 general_comments = q
275
276
276 # pick comments we want to render at current version
277 # pick comments we want to render at current version
277 c.comment_versions = comments_model.aggregate_comments(
278 c.comment_versions = comments_model.aggregate_comments(
278 general_comments, versions, c.at_version_num)
279 general_comments, versions, c.at_version_num)
279
280
280 # INLINE COMMENTS with versions #
281 # INLINE COMMENTS with versions #
281 q = comments_model._all_inline_comments_of_pull_request(pull_request)
282 q = comments_model._all_inline_comments_of_pull_request(pull_request)
282 q = q.order_by(ChangesetComment.comment_id.asc())
283 q = q.order_by(ChangesetComment.comment_id.asc())
283 inline_comments = q
284 inline_comments = q
284
285
285 c.inline_versions = comments_model.aggregate_comments(
286 c.inline_versions = comments_model.aggregate_comments(
286 inline_comments, versions, c.at_version_num, inline=True)
287 inline_comments, versions, c.at_version_num, inline=True)
287
288
288 # Comments inline+general
289 # Comments inline+general
289 if c.at_version:
290 if c.at_version:
290 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
291 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
291 c.comments = c.comment_versions[c.at_version_num]['display']
292 c.comments = c.comment_versions[c.at_version_num]['display']
292 else:
293 else:
293 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
294 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
294 c.comments = c.comment_versions[c.at_version_num]['until']
295 c.comments = c.comment_versions[c.at_version_num]['until']
295
296
296 return general_comments, inline_comments
297 return general_comments, inline_comments
297
298
298 @LoginRequired()
299 @LoginRequired()
299 @HasRepoPermissionAnyDecorator(
300 @HasRepoPermissionAnyDecorator(
300 'repository.read', 'repository.write', 'repository.admin')
301 'repository.read', 'repository.write', 'repository.admin')
301 @view_config(
302 @view_config(
302 route_name='pullrequest_show', request_method='GET',
303 route_name='pullrequest_show', request_method='GET',
303 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
304 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
304 def pull_request_show(self):
305 def pull_request_show(self):
305 _ = self.request.translate
306 _ = self.request.translate
306 c = self.load_default_context()
307 c = self.load_default_context()
307
308
308 pull_request = PullRequest.get_or_404(
309 pull_request = PullRequest.get_or_404(
309 self.request.matchdict['pull_request_id'])
310 self.request.matchdict['pull_request_id'])
310 pull_request_id = pull_request.pull_request_id
311 pull_request_id = pull_request.pull_request_id
311
312
312 c.state_progressing = pull_request.is_state_changing()
313 c.state_progressing = pull_request.is_state_changing()
313 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
314 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
314 pull_request.target_repo.repo_name, pull_request.pull_request_id)
315 pull_request.target_repo.repo_name, pull_request.pull_request_id)
315
316
316 _new_state = {
317 _new_state = {
317 'created': PullRequest.STATE_CREATED,
318 'created': PullRequest.STATE_CREATED,
318 }.get(self.request.GET.get('force_state'))
319 }.get(self.request.GET.get('force_state'))
319
320
320 if c.is_super_admin and _new_state:
321 if c.is_super_admin and _new_state:
321 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
322 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
322 h.flash(
323 h.flash(
323 _('Pull Request state was force changed to `{}`').format(_new_state),
324 _('Pull Request state was force changed to `{}`').format(_new_state),
324 category='success')
325 category='success')
325 Session().commit()
326 Session().commit()
326
327
327 raise HTTPFound(h.route_path(
328 raise HTTPFound(h.route_path(
328 'pullrequest_show', repo_name=self.db_repo_name,
329 'pullrequest_show', repo_name=self.db_repo_name,
329 pull_request_id=pull_request_id))
330 pull_request_id=pull_request_id))
330
331
331 version = self.request.GET.get('version')
332 version = self.request.GET.get('version')
332 from_version = self.request.GET.get('from_version') or version
333 from_version = self.request.GET.get('from_version') or version
333 merge_checks = self.request.GET.get('merge_checks')
334 merge_checks = self.request.GET.get('merge_checks')
334 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
335 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
335 force_refresh = str2bool(self.request.GET.get('force_refresh'))
336 force_refresh = str2bool(self.request.GET.get('force_refresh'))
336 c.range_diff_on = self.request.GET.get('range-diff') == "1"
337 c.range_diff_on = self.request.GET.get('range-diff') == "1"
337
338
338 # fetch global flags of ignore ws or context lines
339 # fetch global flags of ignore ws or context lines
339 diff_context = diffs.get_diff_context(self.request)
340 diff_context = diffs.get_diff_context(self.request)
340 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
341 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
341
342
342 (pull_request_latest,
343 (pull_request_latest,
343 pull_request_at_ver,
344 pull_request_at_ver,
344 pull_request_display_obj,
345 pull_request_display_obj,
345 at_version) = PullRequestModel().get_pr_version(
346 at_version) = PullRequestModel().get_pr_version(
346 pull_request_id, version=version)
347 pull_request_id, version=version)
347
348
348 pr_closed = pull_request_latest.is_closed()
349 pr_closed = pull_request_latest.is_closed()
349
350
350 if pr_closed and (version or from_version):
351 if pr_closed and (version or from_version):
351 # not allow to browse versions for closed PR
352 # not allow to browse versions for closed PR
352 raise HTTPFound(h.route_path(
353 raise HTTPFound(h.route_path(
353 'pullrequest_show', repo_name=self.db_repo_name,
354 'pullrequest_show', repo_name=self.db_repo_name,
354 pull_request_id=pull_request_id))
355 pull_request_id=pull_request_id))
355
356
356 versions = pull_request_display_obj.versions()
357 versions = pull_request_display_obj.versions()
357 # used to store per-commit range diffs
358 # used to store per-commit range diffs
358 c.changes = collections.OrderedDict()
359 c.changes = collections.OrderedDict()
359
360
360 c.at_version = at_version
361 c.at_version = at_version
361 c.at_version_num = (at_version
362 c.at_version_num = (at_version
362 if at_version and at_version != PullRequest.LATEST_VER
363 if at_version and at_version != PullRequest.LATEST_VER
363 else None)
364 else None)
364
365
365 c.at_version_index = ChangesetComment.get_index_from_version(
366 c.at_version_index = ChangesetComment.get_index_from_version(
366 c.at_version_num, versions)
367 c.at_version_num, versions)
367
368
368 (prev_pull_request_latest,
369 (prev_pull_request_latest,
369 prev_pull_request_at_ver,
370 prev_pull_request_at_ver,
370 prev_pull_request_display_obj,
371 prev_pull_request_display_obj,
371 prev_at_version) = PullRequestModel().get_pr_version(
372 prev_at_version) = PullRequestModel().get_pr_version(
372 pull_request_id, version=from_version)
373 pull_request_id, version=from_version)
373
374
374 c.from_version = prev_at_version
375 c.from_version = prev_at_version
375 c.from_version_num = (prev_at_version
376 c.from_version_num = (prev_at_version
376 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
377 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
377 else None)
378 else None)
378 c.from_version_index = ChangesetComment.get_index_from_version(
379 c.from_version_index = ChangesetComment.get_index_from_version(
379 c.from_version_num, versions)
380 c.from_version_num, versions)
380
381
381 # define if we're in COMPARE mode or VIEW at version mode
382 # define if we're in COMPARE mode or VIEW at version mode
382 compare = at_version != prev_at_version
383 compare = at_version != prev_at_version
383
384
384 # pull_requests repo_name we opened it against
385 # pull_requests repo_name we opened it against
385 # ie. target_repo must match
386 # ie. target_repo must match
386 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
387 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
387 log.warning('Mismatch between the current repo: %s, and target %s',
388 log.warning('Mismatch between the current repo: %s, and target %s',
388 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
389 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
389 raise HTTPNotFound()
390 raise HTTPNotFound()
390
391
391 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
392 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
392
393
393 c.pull_request = pull_request_display_obj
394 c.pull_request = pull_request_display_obj
394 c.renderer = pull_request_at_ver.description_renderer or c.renderer
395 c.renderer = pull_request_at_ver.description_renderer or c.renderer
395 c.pull_request_latest = pull_request_latest
396 c.pull_request_latest = pull_request_latest
396
397
397 # inject latest version
398 # inject latest version
398 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
399 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
399 c.versions = versions + [latest_ver]
400 c.versions = versions + [latest_ver]
400
401
401 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
402 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
402 c.allowed_to_change_status = False
403 c.allowed_to_change_status = False
403 c.allowed_to_update = False
404 c.allowed_to_update = False
404 c.allowed_to_merge = False
405 c.allowed_to_merge = False
405 c.allowed_to_delete = False
406 c.allowed_to_delete = False
406 c.allowed_to_comment = False
407 c.allowed_to_comment = False
407 c.allowed_to_close = False
408 c.allowed_to_close = False
408 else:
409 else:
409 can_change_status = PullRequestModel().check_user_change_status(
410 can_change_status = PullRequestModel().check_user_change_status(
410 pull_request_at_ver, self._rhodecode_user)
411 pull_request_at_ver, self._rhodecode_user)
411 c.allowed_to_change_status = can_change_status and not pr_closed
412 c.allowed_to_change_status = can_change_status and not pr_closed
412
413
413 c.allowed_to_update = PullRequestModel().check_user_update(
414 c.allowed_to_update = PullRequestModel().check_user_update(
414 pull_request_latest, self._rhodecode_user) and not pr_closed
415 pull_request_latest, self._rhodecode_user) and not pr_closed
415 c.allowed_to_merge = PullRequestModel().check_user_merge(
416 c.allowed_to_merge = PullRequestModel().check_user_merge(
416 pull_request_latest, self._rhodecode_user) and not pr_closed
417 pull_request_latest, self._rhodecode_user) and not pr_closed
417 c.allowed_to_delete = PullRequestModel().check_user_delete(
418 c.allowed_to_delete = PullRequestModel().check_user_delete(
418 pull_request_latest, self._rhodecode_user) and not pr_closed
419 pull_request_latest, self._rhodecode_user) and not pr_closed
419 c.allowed_to_comment = not pr_closed
420 c.allowed_to_comment = not pr_closed
420 c.allowed_to_close = c.allowed_to_merge and not pr_closed
421 c.allowed_to_close = c.allowed_to_merge and not pr_closed
421
422
422 c.forbid_adding_reviewers = False
423 c.forbid_adding_reviewers = False
423 c.forbid_author_to_review = False
424 c.forbid_author_to_review = False
424 c.forbid_commit_author_to_review = False
425 c.forbid_commit_author_to_review = False
425
426
426 if pull_request_latest.reviewer_data and \
427 if pull_request_latest.reviewer_data and \
427 'rules' in pull_request_latest.reviewer_data:
428 'rules' in pull_request_latest.reviewer_data:
428 rules = pull_request_latest.reviewer_data['rules'] or {}
429 rules = pull_request_latest.reviewer_data['rules'] or {}
429 try:
430 try:
430 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
431 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
431 c.forbid_author_to_review = rules.get('forbid_author_to_review')
432 c.forbid_author_to_review = rules.get('forbid_author_to_review')
432 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
433 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
433 except Exception:
434 except Exception:
434 pass
435 pass
435
436
436 # check merge capabilities
437 # check merge capabilities
437 _merge_check = MergeCheck.validate(
438 _merge_check = MergeCheck.validate(
438 pull_request_latest, auth_user=self._rhodecode_user,
439 pull_request_latest, auth_user=self._rhodecode_user,
439 translator=self.request.translate,
440 translator=self.request.translate,
440 force_shadow_repo_refresh=force_refresh)
441 force_shadow_repo_refresh=force_refresh)
441
442
442 c.pr_merge_errors = _merge_check.error_details
443 c.pr_merge_errors = _merge_check.error_details
443 c.pr_merge_possible = not _merge_check.failed
444 c.pr_merge_possible = not _merge_check.failed
444 c.pr_merge_message = _merge_check.merge_msg
445 c.pr_merge_message = _merge_check.merge_msg
445 c.pr_merge_source_commit = _merge_check.source_commit
446 c.pr_merge_source_commit = _merge_check.source_commit
446 c.pr_merge_target_commit = _merge_check.target_commit
447 c.pr_merge_target_commit = _merge_check.target_commit
447
448
448 c.pr_merge_info = MergeCheck.get_merge_conditions(
449 c.pr_merge_info = MergeCheck.get_merge_conditions(
449 pull_request_latest, translator=self.request.translate)
450 pull_request_latest, translator=self.request.translate)
450
451
451 c.pull_request_review_status = _merge_check.review_status
452 c.pull_request_review_status = _merge_check.review_status
452 if merge_checks:
453 if merge_checks:
453 self.request.override_renderer = \
454 self.request.override_renderer = \
454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
455 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
455 return self._get_template_context(c)
456 return self._get_template_context(c)
456
457
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
458 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
459 c.reviewers_count = pull_request.reviewers_count
460 c.observers_count = pull_request.observers_count
458
461
459 # reviewers and statuses
462 # reviewers and statuses
460 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
461 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
462
466
463 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
464 member_reviewer = h.reviewer_as_json(
468 member_reviewer = h.reviewer_as_json(
465 member, reasons=reasons, mandatory=mandatory,
469 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
466 user_group=review_obj.rule_user_group_data()
471 user_group=review_obj.rule_user_group_data()
467 )
472 )
468
473
469 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
470 member_reviewer['review_status'] = current_review_status
475 member_reviewer['review_status'] = current_review_status
471 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
472 member_reviewer['allowed_to_update'] = c.allowed_to_update
477 member_reviewer['allowed_to_update'] = c.allowed_to_update
473 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
474
479
475 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
476
481
482 for observer_obj, member in pull_request_at_ver.observers():
483 member_observer = h.reviewer_as_json(
484 member, reasons=[], mandatory=False,
485 role=observer_obj.role,
486 user_group=observer_obj.rule_user_group_data()
487 )
488 member_observer['allowed_to_update'] = c.allowed_to_update
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492
477 general_comments, inline_comments = \
493 general_comments, inline_comments = \
478 self.register_comments_vars(c, pull_request_latest, versions)
494 self.register_comments_vars(c, pull_request_latest, versions)
479
495
480 # TODOs
496 # TODOs
481 c.unresolved_comments = CommentsModel() \
497 c.unresolved_comments = CommentsModel() \
482 .get_pull_request_unresolved_todos(pull_request_latest)
498 .get_pull_request_unresolved_todos(pull_request_latest)
483 c.resolved_comments = CommentsModel() \
499 c.resolved_comments = CommentsModel() \
484 .get_pull_request_resolved_todos(pull_request_latest)
500 .get_pull_request_resolved_todos(pull_request_latest)
485
501
486 # if we use version, then do not show later comments
502 # if we use version, then do not show later comments
487 # than current version
503 # than current version
488 display_inline_comments = collections.defaultdict(
504 display_inline_comments = collections.defaultdict(
489 lambda: collections.defaultdict(list))
505 lambda: collections.defaultdict(list))
490 for co in inline_comments:
506 for co in inline_comments:
491 if c.at_version_num:
507 if c.at_version_num:
492 # pick comments that are at least UPTO given version, so we
508 # pick comments that are at least UPTO given version, so we
493 # don't render comments for higher version
509 # don't render comments for higher version
494 should_render = co.pull_request_version_id and \
510 should_render = co.pull_request_version_id and \
495 co.pull_request_version_id <= c.at_version_num
511 co.pull_request_version_id <= c.at_version_num
496 else:
512 else:
497 # showing all, for 'latest'
513 # showing all, for 'latest'
498 should_render = True
514 should_render = True
499
515
500 if should_render:
516 if should_render:
501 display_inline_comments[co.f_path][co.line_no].append(co)
517 display_inline_comments[co.f_path][co.line_no].append(co)
502
518
503 # load diff data into template context, if we use compare mode then
519 # load diff data into template context, if we use compare mode then
504 # diff is calculated based on changes between versions of PR
520 # diff is calculated based on changes between versions of PR
505
521
506 source_repo = pull_request_at_ver.source_repo
522 source_repo = pull_request_at_ver.source_repo
507 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
523 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
508
524
509 target_repo = pull_request_at_ver.target_repo
525 target_repo = pull_request_at_ver.target_repo
510 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
526 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
511
527
512 if compare:
528 if compare:
513 # in compare switch the diff base to latest commit from prev version
529 # in compare switch the diff base to latest commit from prev version
514 target_ref_id = prev_pull_request_display_obj.revisions[0]
530 target_ref_id = prev_pull_request_display_obj.revisions[0]
515
531
516 # despite opening commits for bookmarks/branches/tags, we always
532 # despite opening commits for bookmarks/branches/tags, we always
517 # convert this to rev to prevent changes after bookmark or branch change
533 # convert this to rev to prevent changes after bookmark or branch change
518 c.source_ref_type = 'rev'
534 c.source_ref_type = 'rev'
519 c.source_ref = source_ref_id
535 c.source_ref = source_ref_id
520
536
521 c.target_ref_type = 'rev'
537 c.target_ref_type = 'rev'
522 c.target_ref = target_ref_id
538 c.target_ref = target_ref_id
523
539
524 c.source_repo = source_repo
540 c.source_repo = source_repo
525 c.target_repo = target_repo
541 c.target_repo = target_repo
526
542
527 c.commit_ranges = []
543 c.commit_ranges = []
528 source_commit = EmptyCommit()
544 source_commit = EmptyCommit()
529 target_commit = EmptyCommit()
545 target_commit = EmptyCommit()
530 c.missing_requirements = False
546 c.missing_requirements = False
531
547
532 source_scm = source_repo.scm_instance()
548 source_scm = source_repo.scm_instance()
533 target_scm = target_repo.scm_instance()
549 target_scm = target_repo.scm_instance()
534
550
535 shadow_scm = None
551 shadow_scm = None
536 try:
552 try:
537 shadow_scm = pull_request_latest.get_shadow_repo()
553 shadow_scm = pull_request_latest.get_shadow_repo()
538 except Exception:
554 except Exception:
539 log.debug('Failed to get shadow repo', exc_info=True)
555 log.debug('Failed to get shadow repo', exc_info=True)
540 # try first the existing source_repo, and then shadow
556 # try first the existing source_repo, and then shadow
541 # repo if we can obtain one
557 # repo if we can obtain one
542 commits_source_repo = source_scm
558 commits_source_repo = source_scm
543 if shadow_scm:
559 if shadow_scm:
544 commits_source_repo = shadow_scm
560 commits_source_repo = shadow_scm
545
561
546 c.commits_source_repo = commits_source_repo
562 c.commits_source_repo = commits_source_repo
547 c.ancestor = None # set it to None, to hide it from PR view
563 c.ancestor = None # set it to None, to hide it from PR view
548
564
549 # empty version means latest, so we keep this to prevent
565 # empty version means latest, so we keep this to prevent
550 # double caching
566 # double caching
551 version_normalized = version or PullRequest.LATEST_VER
567 version_normalized = version or PullRequest.LATEST_VER
552 from_version_normalized = from_version or PullRequest.LATEST_VER
568 from_version_normalized = from_version or PullRequest.LATEST_VER
553
569
554 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
570 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
555 cache_file_path = diff_cache_exist(
571 cache_file_path = diff_cache_exist(
556 cache_path, 'pull_request', pull_request_id, version_normalized,
572 cache_path, 'pull_request', pull_request_id, version_normalized,
557 from_version_normalized, source_ref_id, target_ref_id,
573 from_version_normalized, source_ref_id, target_ref_id,
558 hide_whitespace_changes, diff_context, c.fulldiff)
574 hide_whitespace_changes, diff_context, c.fulldiff)
559
575
560 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
576 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
561 force_recache = self.get_recache_flag()
577 force_recache = self.get_recache_flag()
562
578
563 cached_diff = None
579 cached_diff = None
564 if caching_enabled:
580 if caching_enabled:
565 cached_diff = load_cached_diff(cache_file_path)
581 cached_diff = load_cached_diff(cache_file_path)
566
582
567 has_proper_commit_cache = (
583 has_proper_commit_cache = (
568 cached_diff and cached_diff.get('commits')
584 cached_diff and cached_diff.get('commits')
569 and len(cached_diff.get('commits', [])) == 5
585 and len(cached_diff.get('commits', [])) == 5
570 and cached_diff.get('commits')[0]
586 and cached_diff.get('commits')[0]
571 and cached_diff.get('commits')[3])
587 and cached_diff.get('commits')[3])
572
588
573 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
589 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
574 diff_commit_cache = \
590 diff_commit_cache = \
575 (ancestor_commit, commit_cache, missing_requirements,
591 (ancestor_commit, commit_cache, missing_requirements,
576 source_commit, target_commit) = cached_diff['commits']
592 source_commit, target_commit) = cached_diff['commits']
577 else:
593 else:
578 # NOTE(marcink): we reach potentially unreachable errors when a PR has
594 # NOTE(marcink): we reach potentially unreachable errors when a PR has
579 # merge errors resulting in potentially hidden commits in the shadow repo.
595 # merge errors resulting in potentially hidden commits in the shadow repo.
580 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
596 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
581 and _merge_check.merge_response
597 and _merge_check.merge_response
582 maybe_unreachable = maybe_unreachable \
598 maybe_unreachable = maybe_unreachable \
583 and _merge_check.merge_response.metadata.get('unresolved_files')
599 and _merge_check.merge_response.metadata.get('unresolved_files')
584 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
600 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
585 diff_commit_cache = \
601 diff_commit_cache = \
586 (ancestor_commit, commit_cache, missing_requirements,
602 (ancestor_commit, commit_cache, missing_requirements,
587 source_commit, target_commit) = self.get_commits(
603 source_commit, target_commit) = self.get_commits(
588 commits_source_repo,
604 commits_source_repo,
589 pull_request_at_ver,
605 pull_request_at_ver,
590 source_commit,
606 source_commit,
591 source_ref_id,
607 source_ref_id,
592 source_scm,
608 source_scm,
593 target_commit,
609 target_commit,
594 target_ref_id,
610 target_ref_id,
595 target_scm,
611 target_scm,
596 maybe_unreachable=maybe_unreachable)
612 maybe_unreachable=maybe_unreachable)
597
613
598 # register our commit range
614 # register our commit range
599 for comm in commit_cache.values():
615 for comm in commit_cache.values():
600 c.commit_ranges.append(comm)
616 c.commit_ranges.append(comm)
601
617
602 c.missing_requirements = missing_requirements
618 c.missing_requirements = missing_requirements
603 c.ancestor_commit = ancestor_commit
619 c.ancestor_commit = ancestor_commit
604 c.statuses = source_repo.statuses(
620 c.statuses = source_repo.statuses(
605 [x.raw_id for x in c.commit_ranges])
621 [x.raw_id for x in c.commit_ranges])
606
622
607 # auto collapse if we have more than limit
623 # auto collapse if we have more than limit
608 collapse_limit = diffs.DiffProcessor._collapse_commits_over
624 collapse_limit = diffs.DiffProcessor._collapse_commits_over
609 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
625 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
610 c.compare_mode = compare
626 c.compare_mode = compare
611
627
612 # diff_limit is the old behavior, will cut off the whole diff
628 # diff_limit is the old behavior, will cut off the whole diff
613 # if the limit is applied otherwise will just hide the
629 # if the limit is applied otherwise will just hide the
614 # big files from the front-end
630 # big files from the front-end
615 diff_limit = c.visual.cut_off_limit_diff
631 diff_limit = c.visual.cut_off_limit_diff
616 file_limit = c.visual.cut_off_limit_file
632 file_limit = c.visual.cut_off_limit_file
617
633
618 c.missing_commits = False
634 c.missing_commits = False
619 if (c.missing_requirements
635 if (c.missing_requirements
620 or isinstance(source_commit, EmptyCommit)
636 or isinstance(source_commit, EmptyCommit)
621 or source_commit == target_commit):
637 or source_commit == target_commit):
622
638
623 c.missing_commits = True
639 c.missing_commits = True
624 else:
640 else:
625 c.inline_comments = display_inline_comments
641 c.inline_comments = display_inline_comments
626
642
627 use_ancestor = True
643 use_ancestor = True
628 if from_version_normalized != version_normalized:
644 if from_version_normalized != version_normalized:
629 use_ancestor = False
645 use_ancestor = False
630
646
631 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
647 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
632 if not force_recache and has_proper_diff_cache:
648 if not force_recache and has_proper_diff_cache:
633 c.diffset = cached_diff['diff']
649 c.diffset = cached_diff['diff']
634 else:
650 else:
635 try:
651 try:
636 c.diffset = self._get_diffset(
652 c.diffset = self._get_diffset(
637 c.source_repo.repo_name, commits_source_repo,
653 c.source_repo.repo_name, commits_source_repo,
638 c.ancestor_commit,
654 c.ancestor_commit,
639 source_ref_id, target_ref_id,
655 source_ref_id, target_ref_id,
640 target_commit, source_commit,
656 target_commit, source_commit,
641 diff_limit, file_limit, c.fulldiff,
657 diff_limit, file_limit, c.fulldiff,
642 hide_whitespace_changes, diff_context,
658 hide_whitespace_changes, diff_context,
643 use_ancestor=use_ancestor
659 use_ancestor=use_ancestor
644 )
660 )
645
661
646 # save cached diff
662 # save cached diff
647 if caching_enabled:
663 if caching_enabled:
648 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
664 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
649 except CommitDoesNotExistError:
665 except CommitDoesNotExistError:
650 log.exception('Failed to generate diffset')
666 log.exception('Failed to generate diffset')
651 c.missing_commits = True
667 c.missing_commits = True
652
668
653 if not c.missing_commits:
669 if not c.missing_commits:
654
670
655 c.limited_diff = c.diffset.limited_diff
671 c.limited_diff = c.diffset.limited_diff
656
672
657 # calculate removed files that are bound to comments
673 # calculate removed files that are bound to comments
658 comment_deleted_files = [
674 comment_deleted_files = [
659 fname for fname in display_inline_comments
675 fname for fname in display_inline_comments
660 if fname not in c.diffset.file_stats]
676 if fname not in c.diffset.file_stats]
661
677
662 c.deleted_files_comments = collections.defaultdict(dict)
678 c.deleted_files_comments = collections.defaultdict(dict)
663 for fname, per_line_comments in display_inline_comments.items():
679 for fname, per_line_comments in display_inline_comments.items():
664 if fname in comment_deleted_files:
680 if fname in comment_deleted_files:
665 c.deleted_files_comments[fname]['stats'] = 0
681 c.deleted_files_comments[fname]['stats'] = 0
666 c.deleted_files_comments[fname]['comments'] = list()
682 c.deleted_files_comments[fname]['comments'] = list()
667 for lno, comments in per_line_comments.items():
683 for lno, comments in per_line_comments.items():
668 c.deleted_files_comments[fname]['comments'].extend(comments)
684 c.deleted_files_comments[fname]['comments'].extend(comments)
669
685
670 # maybe calculate the range diff
686 # maybe calculate the range diff
671 if c.range_diff_on:
687 if c.range_diff_on:
672 # TODO(marcink): set whitespace/context
688 # TODO(marcink): set whitespace/context
673 context_lcl = 3
689 context_lcl = 3
674 ign_whitespace_lcl = False
690 ign_whitespace_lcl = False
675
691
676 for commit in c.commit_ranges:
692 for commit in c.commit_ranges:
677 commit2 = commit
693 commit2 = commit
678 commit1 = commit.first_parent
694 commit1 = commit.first_parent
679
695
680 range_diff_cache_file_path = diff_cache_exist(
696 range_diff_cache_file_path = diff_cache_exist(
681 cache_path, 'diff', commit.raw_id,
697 cache_path, 'diff', commit.raw_id,
682 ign_whitespace_lcl, context_lcl, c.fulldiff)
698 ign_whitespace_lcl, context_lcl, c.fulldiff)
683
699
684 cached_diff = None
700 cached_diff = None
685 if caching_enabled:
701 if caching_enabled:
686 cached_diff = load_cached_diff(range_diff_cache_file_path)
702 cached_diff = load_cached_diff(range_diff_cache_file_path)
687
703
688 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
704 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
689 if not force_recache and has_proper_diff_cache:
705 if not force_recache and has_proper_diff_cache:
690 diffset = cached_diff['diff']
706 diffset = cached_diff['diff']
691 else:
707 else:
692 diffset = self._get_range_diffset(
708 diffset = self._get_range_diffset(
693 commits_source_repo, source_repo,
709 commits_source_repo, source_repo,
694 commit1, commit2, diff_limit, file_limit,
710 commit1, commit2, diff_limit, file_limit,
695 c.fulldiff, ign_whitespace_lcl, context_lcl
711 c.fulldiff, ign_whitespace_lcl, context_lcl
696 )
712 )
697
713
698 # save cached diff
714 # save cached diff
699 if caching_enabled:
715 if caching_enabled:
700 cache_diff(range_diff_cache_file_path, diffset, None)
716 cache_diff(range_diff_cache_file_path, diffset, None)
701
717
702 c.changes[commit.raw_id] = diffset
718 c.changes[commit.raw_id] = diffset
703
719
704 # this is a hack to properly display links, when creating PR, the
720 # this is a hack to properly display links, when creating PR, the
705 # compare view and others uses different notation, and
721 # compare view and others uses different notation, and
706 # compare_commits.mako renders links based on the target_repo.
722 # compare_commits.mako renders links based on the target_repo.
707 # We need to swap that here to generate it properly on the html side
723 # We need to swap that here to generate it properly on the html side
708 c.target_repo = c.source_repo
724 c.target_repo = c.source_repo
709
725
710 c.commit_statuses = ChangesetStatus.STATUSES
726 c.commit_statuses = ChangesetStatus.STATUSES
711
727
712 c.show_version_changes = not pr_closed
728 c.show_version_changes = not pr_closed
713 if c.show_version_changes:
729 if c.show_version_changes:
714 cur_obj = pull_request_at_ver
730 cur_obj = pull_request_at_ver
715 prev_obj = prev_pull_request_at_ver
731 prev_obj = prev_pull_request_at_ver
716
732
717 old_commit_ids = prev_obj.revisions
733 old_commit_ids = prev_obj.revisions
718 new_commit_ids = cur_obj.revisions
734 new_commit_ids = cur_obj.revisions
719 commit_changes = PullRequestModel()._calculate_commit_id_changes(
735 commit_changes = PullRequestModel()._calculate_commit_id_changes(
720 old_commit_ids, new_commit_ids)
736 old_commit_ids, new_commit_ids)
721 c.commit_changes_summary = commit_changes
737 c.commit_changes_summary = commit_changes
722
738
723 # calculate the diff for commits between versions
739 # calculate the diff for commits between versions
724 c.commit_changes = []
740 c.commit_changes = []
725
741
726 def mark(cs, fw):
742 def mark(cs, fw):
727 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
743 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
728
744
729 for c_type, raw_id in mark(commit_changes.added, 'a') \
745 for c_type, raw_id in mark(commit_changes.added, 'a') \
730 + mark(commit_changes.removed, 'r') \
746 + mark(commit_changes.removed, 'r') \
731 + mark(commit_changes.common, 'c'):
747 + mark(commit_changes.common, 'c'):
732
748
733 if raw_id in commit_cache:
749 if raw_id in commit_cache:
734 commit = commit_cache[raw_id]
750 commit = commit_cache[raw_id]
735 else:
751 else:
736 try:
752 try:
737 commit = commits_source_repo.get_commit(raw_id)
753 commit = commits_source_repo.get_commit(raw_id)
738 except CommitDoesNotExistError:
754 except CommitDoesNotExistError:
739 # in case we fail extracting still use "dummy" commit
755 # in case we fail extracting still use "dummy" commit
740 # for display in commit diff
756 # for display in commit diff
741 commit = h.AttributeDict(
757 commit = h.AttributeDict(
742 {'raw_id': raw_id,
758 {'raw_id': raw_id,
743 'message': 'EMPTY or MISSING COMMIT'})
759 'message': 'EMPTY or MISSING COMMIT'})
744 c.commit_changes.append([c_type, commit])
760 c.commit_changes.append([c_type, commit])
745
761
746 # current user review statuses for each version
762 # current user review statuses for each version
747 c.review_versions = {}
763 c.review_versions = {}
748 if self._rhodecode_user.user_id in c.allowed_reviewers:
764 if self._rhodecode_user.user_id in c.allowed_reviewers:
749 for co in general_comments:
765 for co in general_comments:
750 if co.author.user_id == self._rhodecode_user.user_id:
766 if co.author.user_id == self._rhodecode_user.user_id:
751 status = co.status_change
767 status = co.status_change
752 if status:
768 if status:
753 _ver_pr = status[0].comment.pull_request_version_id
769 _ver_pr = status[0].comment.pull_request_version_id
754 c.review_versions[_ver_pr] = status[0]
770 c.review_versions[_ver_pr] = status[0]
755
771
756 return self._get_template_context(c)
772 return self._get_template_context(c)
757
773
758 def get_commits(
774 def get_commits(
759 self, commits_source_repo, pull_request_at_ver, source_commit,
775 self, commits_source_repo, pull_request_at_ver, source_commit,
760 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
776 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
761 maybe_unreachable=False):
777 maybe_unreachable=False):
762
778
763 commit_cache = collections.OrderedDict()
779 commit_cache = collections.OrderedDict()
764 missing_requirements = False
780 missing_requirements = False
765
781
766 try:
782 try:
767 pre_load = ["author", "date", "message", "branch", "parents"]
783 pre_load = ["author", "date", "message", "branch", "parents"]
768
784
769 pull_request_commits = pull_request_at_ver.revisions
785 pull_request_commits = pull_request_at_ver.revisions
770 log.debug('Loading %s commits from %s',
786 log.debug('Loading %s commits from %s',
771 len(pull_request_commits), commits_source_repo)
787 len(pull_request_commits), commits_source_repo)
772
788
773 for rev in pull_request_commits:
789 for rev in pull_request_commits:
774 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
790 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
775 maybe_unreachable=maybe_unreachable)
791 maybe_unreachable=maybe_unreachable)
776 commit_cache[comm.raw_id] = comm
792 commit_cache[comm.raw_id] = comm
777
793
778 # Order here matters, we first need to get target, and then
794 # Order here matters, we first need to get target, and then
779 # the source
795 # the source
780 target_commit = commits_source_repo.get_commit(
796 target_commit = commits_source_repo.get_commit(
781 commit_id=safe_str(target_ref_id))
797 commit_id=safe_str(target_ref_id))
782
798
783 source_commit = commits_source_repo.get_commit(
799 source_commit = commits_source_repo.get_commit(
784 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
800 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
785 except CommitDoesNotExistError:
801 except CommitDoesNotExistError:
786 log.warning('Failed to get commit from `{}` repo'.format(
802 log.warning('Failed to get commit from `{}` repo'.format(
787 commits_source_repo), exc_info=True)
803 commits_source_repo), exc_info=True)
788 except RepositoryRequirementError:
804 except RepositoryRequirementError:
789 log.warning('Failed to get all required data from repo', exc_info=True)
805 log.warning('Failed to get all required data from repo', exc_info=True)
790 missing_requirements = True
806 missing_requirements = True
791
807
792 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
808 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
793
809
794 try:
810 try:
795 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
811 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
796 except Exception:
812 except Exception:
797 ancestor_commit = None
813 ancestor_commit = None
798
814
799 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
815 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
800
816
801 def assure_not_empty_repo(self):
817 def assure_not_empty_repo(self):
802 _ = self.request.translate
818 _ = self.request.translate
803
819
804 try:
820 try:
805 self.db_repo.scm_instance().get_commit()
821 self.db_repo.scm_instance().get_commit()
806 except EmptyRepositoryError:
822 except EmptyRepositoryError:
807 h.flash(h.literal(_('There are no commits yet')),
823 h.flash(h.literal(_('There are no commits yet')),
808 category='warning')
824 category='warning')
809 raise HTTPFound(
825 raise HTTPFound(
810 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
826 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
811
827
812 @LoginRequired()
828 @LoginRequired()
813 @NotAnonymous()
829 @NotAnonymous()
814 @HasRepoPermissionAnyDecorator(
830 @HasRepoPermissionAnyDecorator(
815 'repository.read', 'repository.write', 'repository.admin')
831 'repository.read', 'repository.write', 'repository.admin')
816 @view_config(
832 @view_config(
817 route_name='pullrequest_new', request_method='GET',
833 route_name='pullrequest_new', request_method='GET',
818 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
834 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
819 def pull_request_new(self):
835 def pull_request_new(self):
820 _ = self.request.translate
836 _ = self.request.translate
821 c = self.load_default_context()
837 c = self.load_default_context()
822
838
823 self.assure_not_empty_repo()
839 self.assure_not_empty_repo()
824 source_repo = self.db_repo
840 source_repo = self.db_repo
825
841
826 commit_id = self.request.GET.get('commit')
842 commit_id = self.request.GET.get('commit')
827 branch_ref = self.request.GET.get('branch')
843 branch_ref = self.request.GET.get('branch')
828 bookmark_ref = self.request.GET.get('bookmark')
844 bookmark_ref = self.request.GET.get('bookmark')
829
845
830 try:
846 try:
831 source_repo_data = PullRequestModel().generate_repo_data(
847 source_repo_data = PullRequestModel().generate_repo_data(
832 source_repo, commit_id=commit_id,
848 source_repo, commit_id=commit_id,
833 branch=branch_ref, bookmark=bookmark_ref,
849 branch=branch_ref, bookmark=bookmark_ref,
834 translator=self.request.translate)
850 translator=self.request.translate)
835 except CommitDoesNotExistError as e:
851 except CommitDoesNotExistError as e:
836 log.exception(e)
852 log.exception(e)
837 h.flash(_('Commit does not exist'), 'error')
853 h.flash(_('Commit does not exist'), 'error')
838 raise HTTPFound(
854 raise HTTPFound(
839 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
855 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
840
856
841 default_target_repo = source_repo
857 default_target_repo = source_repo
842
858
843 if source_repo.parent and c.has_origin_repo_read_perm:
859 if source_repo.parent and c.has_origin_repo_read_perm:
844 parent_vcs_obj = source_repo.parent.scm_instance()
860 parent_vcs_obj = source_repo.parent.scm_instance()
845 if parent_vcs_obj and not parent_vcs_obj.is_empty():
861 if parent_vcs_obj and not parent_vcs_obj.is_empty():
846 # change default if we have a parent repo
862 # change default if we have a parent repo
847 default_target_repo = source_repo.parent
863 default_target_repo = source_repo.parent
848
864
849 target_repo_data = PullRequestModel().generate_repo_data(
865 target_repo_data = PullRequestModel().generate_repo_data(
850 default_target_repo, translator=self.request.translate)
866 default_target_repo, translator=self.request.translate)
851
867
852 selected_source_ref = source_repo_data['refs']['selected_ref']
868 selected_source_ref = source_repo_data['refs']['selected_ref']
853 title_source_ref = ''
869 title_source_ref = ''
854 if selected_source_ref:
870 if selected_source_ref:
855 title_source_ref = selected_source_ref.split(':', 2)[1]
871 title_source_ref = selected_source_ref.split(':', 2)[1]
856 c.default_title = PullRequestModel().generate_pullrequest_title(
872 c.default_title = PullRequestModel().generate_pullrequest_title(
857 source=source_repo.repo_name,
873 source=source_repo.repo_name,
858 source_ref=title_source_ref,
874 source_ref=title_source_ref,
859 target=default_target_repo.repo_name
875 target=default_target_repo.repo_name
860 )
876 )
861
877
862 c.default_repo_data = {
878 c.default_repo_data = {
863 'source_repo_name': source_repo.repo_name,
879 'source_repo_name': source_repo.repo_name,
864 'source_refs_json': json.dumps(source_repo_data),
880 'source_refs_json': json.dumps(source_repo_data),
865 'target_repo_name': default_target_repo.repo_name,
881 'target_repo_name': default_target_repo.repo_name,
866 'target_refs_json': json.dumps(target_repo_data),
882 'target_refs_json': json.dumps(target_repo_data),
867 }
883 }
868 c.default_source_ref = selected_source_ref
884 c.default_source_ref = selected_source_ref
869
885
870 return self._get_template_context(c)
886 return self._get_template_context(c)
871
887
872 @LoginRequired()
888 @LoginRequired()
873 @NotAnonymous()
889 @NotAnonymous()
874 @HasRepoPermissionAnyDecorator(
890 @HasRepoPermissionAnyDecorator(
875 'repository.read', 'repository.write', 'repository.admin')
891 'repository.read', 'repository.write', 'repository.admin')
876 @view_config(
892 @view_config(
877 route_name='pullrequest_repo_refs', request_method='GET',
893 route_name='pullrequest_repo_refs', request_method='GET',
878 renderer='json_ext', xhr=True)
894 renderer='json_ext', xhr=True)
879 def pull_request_repo_refs(self):
895 def pull_request_repo_refs(self):
880 self.load_default_context()
896 self.load_default_context()
881 target_repo_name = self.request.matchdict['target_repo_name']
897 target_repo_name = self.request.matchdict['target_repo_name']
882 repo = Repository.get_by_repo_name(target_repo_name)
898 repo = Repository.get_by_repo_name(target_repo_name)
883 if not repo:
899 if not repo:
884 raise HTTPNotFound()
900 raise HTTPNotFound()
885
901
886 target_perm = HasRepoPermissionAny(
902 target_perm = HasRepoPermissionAny(
887 'repository.read', 'repository.write', 'repository.admin')(
903 'repository.read', 'repository.write', 'repository.admin')(
888 target_repo_name)
904 target_repo_name)
889 if not target_perm:
905 if not target_perm:
890 raise HTTPNotFound()
906 raise HTTPNotFound()
891
907
892 return PullRequestModel().generate_repo_data(
908 return PullRequestModel().generate_repo_data(
893 repo, translator=self.request.translate)
909 repo, translator=self.request.translate)
894
910
895 @LoginRequired()
911 @LoginRequired()
896 @NotAnonymous()
912 @NotAnonymous()
897 @HasRepoPermissionAnyDecorator(
913 @HasRepoPermissionAnyDecorator(
898 'repository.read', 'repository.write', 'repository.admin')
914 'repository.read', 'repository.write', 'repository.admin')
899 @view_config(
915 @view_config(
900 route_name='pullrequest_repo_targets', request_method='GET',
916 route_name='pullrequest_repo_targets', request_method='GET',
901 renderer='json_ext', xhr=True)
917 renderer='json_ext', xhr=True)
902 def pullrequest_repo_targets(self):
918 def pullrequest_repo_targets(self):
903 _ = self.request.translate
919 _ = self.request.translate
904 filter_query = self.request.GET.get('query')
920 filter_query = self.request.GET.get('query')
905
921
906 # get the parents
922 # get the parents
907 parent_target_repos = []
923 parent_target_repos = []
908 if self.db_repo.parent:
924 if self.db_repo.parent:
909 parents_query = Repository.query() \
925 parents_query = Repository.query() \
910 .order_by(func.length(Repository.repo_name)) \
926 .order_by(func.length(Repository.repo_name)) \
911 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
927 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
912
928
913 if filter_query:
929 if filter_query:
914 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
930 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
915 parents_query = parents_query.filter(
931 parents_query = parents_query.filter(
916 Repository.repo_name.ilike(ilike_expression))
932 Repository.repo_name.ilike(ilike_expression))
917 parents = parents_query.limit(20).all()
933 parents = parents_query.limit(20).all()
918
934
919 for parent in parents:
935 for parent in parents:
920 parent_vcs_obj = parent.scm_instance()
936 parent_vcs_obj = parent.scm_instance()
921 if parent_vcs_obj and not parent_vcs_obj.is_empty():
937 if parent_vcs_obj and not parent_vcs_obj.is_empty():
922 parent_target_repos.append(parent)
938 parent_target_repos.append(parent)
923
939
924 # get other forks, and repo itself
940 # get other forks, and repo itself
925 query = Repository.query() \
941 query = Repository.query() \
926 .order_by(func.length(Repository.repo_name)) \
942 .order_by(func.length(Repository.repo_name)) \
927 .filter(
943 .filter(
928 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
944 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
929 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
945 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
930 ) \
946 ) \
931 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
947 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
932
948
933 if filter_query:
949 if filter_query:
934 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
950 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
935 query = query.filter(Repository.repo_name.ilike(ilike_expression))
951 query = query.filter(Repository.repo_name.ilike(ilike_expression))
936
952
937 limit = max(20 - len(parent_target_repos), 5) # not less then 5
953 limit = max(20 - len(parent_target_repos), 5) # not less then 5
938 target_repos = query.limit(limit).all()
954 target_repos = query.limit(limit).all()
939
955
940 all_target_repos = target_repos + parent_target_repos
956 all_target_repos = target_repos + parent_target_repos
941
957
942 repos = []
958 repos = []
943 # This checks permissions to the repositories
959 # This checks permissions to the repositories
944 for obj in ScmModel().get_repos(all_target_repos):
960 for obj in ScmModel().get_repos(all_target_repos):
945 repos.append({
961 repos.append({
946 'id': obj['name'],
962 'id': obj['name'],
947 'text': obj['name'],
963 'text': obj['name'],
948 'type': 'repo',
964 'type': 'repo',
949 'repo_id': obj['dbrepo']['repo_id'],
965 'repo_id': obj['dbrepo']['repo_id'],
950 'repo_type': obj['dbrepo']['repo_type'],
966 'repo_type': obj['dbrepo']['repo_type'],
951 'private': obj['dbrepo']['private'],
967 'private': obj['dbrepo']['private'],
952
968
953 })
969 })
954
970
955 data = {
971 data = {
956 'more': False,
972 'more': False,
957 'results': [{
973 'results': [{
958 'text': _('Repositories'),
974 'text': _('Repositories'),
959 'children': repos
975 'children': repos
960 }] if repos else []
976 }] if repos else []
961 }
977 }
962 return data
978 return data
963
979
964 @LoginRequired()
980 @LoginRequired()
965 @NotAnonymous()
981 @NotAnonymous()
966 @HasRepoPermissionAnyDecorator(
982 @HasRepoPermissionAnyDecorator(
967 'repository.read', 'repository.write', 'repository.admin')
983 'repository.read', 'repository.write', 'repository.admin')
968 @view_config(
984 @view_config(
969 route_name='pullrequest_comments', request_method='POST',
985 route_name='pullrequest_comments', request_method='POST',
970 renderer='string', xhr=True)
986 renderer='string_html', xhr=True)
971 def pullrequest_comments(self):
987 def pullrequest_comments(self):
972 self.load_default_context()
988 self.load_default_context()
973
989
974 pull_request = PullRequest.get_or_404(
990 pull_request = PullRequest.get_or_404(
975 self.request.matchdict['pull_request_id'])
991 self.request.matchdict['pull_request_id'])
976 pull_request_id = pull_request.pull_request_id
992 pull_request_id = pull_request.pull_request_id
977 version = self.request.GET.get('version')
993 version = self.request.GET.get('version')
978
994
979 _render = self.request.get_partial_renderer(
995 _render = self.request.get_partial_renderer(
980 'rhodecode:templates/base/sidebar.mako')
996 'rhodecode:templates/base/sidebar.mako')
981 c = _render.get_call_context()
997 c = _render.get_call_context()
982
998
983 (pull_request_latest,
999 (pull_request_latest,
984 pull_request_at_ver,
1000 pull_request_at_ver,
985 pull_request_display_obj,
1001 pull_request_display_obj,
986 at_version) = PullRequestModel().get_pr_version(
1002 at_version) = PullRequestModel().get_pr_version(
987 pull_request_id, version=version)
1003 pull_request_id, version=version)
988 versions = pull_request_display_obj.versions()
1004 versions = pull_request_display_obj.versions()
989 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1005 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
990 c.versions = versions + [latest_ver]
1006 c.versions = versions + [latest_ver]
991
1007
992 c.at_version = at_version
1008 c.at_version = at_version
993 c.at_version_num = (at_version
1009 c.at_version_num = (at_version
994 if at_version and at_version != PullRequest.LATEST_VER
1010 if at_version and at_version != PullRequest.LATEST_VER
995 else None)
1011 else None)
996
1012
997 self.register_comments_vars(c, pull_request_latest, versions)
1013 self.register_comments_vars(c, pull_request_latest, versions)
998 all_comments = c.inline_comments_flat + c.comments
1014 all_comments = c.inline_comments_flat + c.comments
999
1015
1000 existing_ids = filter(
1016 existing_ids = filter(
1001 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1017 lambda e: e, map(safe_int, aslist(self.request.POST.get('comments'))))
1018
1002 return _render('comments_table', all_comments, len(all_comments),
1019 return _render('comments_table', all_comments, len(all_comments),
1003 existing_ids=existing_ids)
1020 existing_ids=existing_ids)
1004
1021
1005 @LoginRequired()
1022 @LoginRequired()
1006 @NotAnonymous()
1023 @NotAnonymous()
1007 @HasRepoPermissionAnyDecorator(
1024 @HasRepoPermissionAnyDecorator(
1008 'repository.read', 'repository.write', 'repository.admin')
1025 'repository.read', 'repository.write', 'repository.admin')
1009 @view_config(
1026 @view_config(
1010 route_name='pullrequest_todos', request_method='POST',
1027 route_name='pullrequest_todos', request_method='POST',
1011 renderer='string', xhr=True)
1028 renderer='string_html', xhr=True)
1012 def pullrequest_todos(self):
1029 def pullrequest_todos(self):
1013 self.load_default_context()
1030 self.load_default_context()
1014
1031
1015 pull_request = PullRequest.get_or_404(
1032 pull_request = PullRequest.get_or_404(
1016 self.request.matchdict['pull_request_id'])
1033 self.request.matchdict['pull_request_id'])
1017 pull_request_id = pull_request.pull_request_id
1034 pull_request_id = pull_request.pull_request_id
1018 version = self.request.GET.get('version')
1035 version = self.request.GET.get('version')
1019
1036
1020 _render = self.request.get_partial_renderer(
1037 _render = self.request.get_partial_renderer(
1021 'rhodecode:templates/base/sidebar.mako')
1038 'rhodecode:templates/base/sidebar.mako')
1022 c = _render.get_call_context()
1039 c = _render.get_call_context()
1023 (pull_request_latest,
1040 (pull_request_latest,
1024 pull_request_at_ver,
1041 pull_request_at_ver,
1025 pull_request_display_obj,
1042 pull_request_display_obj,
1026 at_version) = PullRequestModel().get_pr_version(
1043 at_version) = PullRequestModel().get_pr_version(
1027 pull_request_id, version=version)
1044 pull_request_id, version=version)
1028 versions = pull_request_display_obj.versions()
1045 versions = pull_request_display_obj.versions()
1029 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1046 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1030 c.versions = versions + [latest_ver]
1047 c.versions = versions + [latest_ver]
1031
1048
1032 c.at_version = at_version
1049 c.at_version = at_version
1033 c.at_version_num = (at_version
1050 c.at_version_num = (at_version
1034 if at_version and at_version != PullRequest.LATEST_VER
1051 if at_version and at_version != PullRequest.LATEST_VER
1035 else None)
1052 else None)
1036
1053
1037 c.unresolved_comments = CommentsModel() \
1054 c.unresolved_comments = CommentsModel() \
1038 .get_pull_request_unresolved_todos(pull_request)
1055 .get_pull_request_unresolved_todos(pull_request)
1039 c.resolved_comments = CommentsModel() \
1056 c.resolved_comments = CommentsModel() \
1040 .get_pull_request_resolved_todos(pull_request)
1057 .get_pull_request_resolved_todos(pull_request)
1041
1058
1042 all_comments = c.unresolved_comments + c.resolved_comments
1059 all_comments = c.unresolved_comments + c.resolved_comments
1043 existing_ids = filter(
1060 existing_ids = filter(
1044 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1061 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1045 return _render('comments_table', all_comments, len(c.unresolved_comments),
1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1046 todo_comments=True, existing_ids=existing_ids)
1063 todo_comments=True, existing_ids=existing_ids)
1047
1064
1048 @LoginRequired()
1065 @LoginRequired()
1049 @NotAnonymous()
1066 @NotAnonymous()
1050 @HasRepoPermissionAnyDecorator(
1067 @HasRepoPermissionAnyDecorator(
1051 'repository.read', 'repository.write', 'repository.admin')
1068 'repository.read', 'repository.write', 'repository.admin')
1052 @CSRFRequired()
1069 @CSRFRequired()
1053 @view_config(
1070 @view_config(
1054 route_name='pullrequest_create', request_method='POST',
1071 route_name='pullrequest_create', request_method='POST',
1055 renderer=None)
1072 renderer=None)
1056 def pull_request_create(self):
1073 def pull_request_create(self):
1057 _ = self.request.translate
1074 _ = self.request.translate
1058 self.assure_not_empty_repo()
1075 self.assure_not_empty_repo()
1059 self.load_default_context()
1076 self.load_default_context()
1060
1077
1061 controls = peppercorn.parse(self.request.POST.items())
1078 controls = peppercorn.parse(self.request.POST.items())
1062
1079
1063 try:
1080 try:
1064 form = PullRequestForm(
1081 form = PullRequestForm(
1065 self.request.translate, self.db_repo.repo_id)()
1082 self.request.translate, self.db_repo.repo_id)()
1066 _form = form.to_python(controls)
1083 _form = form.to_python(controls)
1067 except formencode.Invalid as errors:
1084 except formencode.Invalid as errors:
1068 if errors.error_dict.get('revisions'):
1085 if errors.error_dict.get('revisions'):
1069 msg = 'Revisions: %s' % errors.error_dict['revisions']
1086 msg = 'Revisions: %s' % errors.error_dict['revisions']
1070 elif errors.error_dict.get('pullrequest_title'):
1087 elif errors.error_dict.get('pullrequest_title'):
1071 msg = errors.error_dict.get('pullrequest_title')
1088 msg = errors.error_dict.get('pullrequest_title')
1072 else:
1089 else:
1073 msg = _('Error creating pull request: {}').format(errors)
1090 msg = _('Error creating pull request: {}').format(errors)
1074 log.exception(msg)
1091 log.exception(msg)
1075 h.flash(msg, 'error')
1092 h.flash(msg, 'error')
1076
1093
1077 # would rather just go back to form ...
1094 # would rather just go back to form ...
1078 raise HTTPFound(
1095 raise HTTPFound(
1079 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1096 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1080
1097
1081 source_repo = _form['source_repo']
1098 source_repo = _form['source_repo']
1082 source_ref = _form['source_ref']
1099 source_ref = _form['source_ref']
1083 target_repo = _form['target_repo']
1100 target_repo = _form['target_repo']
1084 target_ref = _form['target_ref']
1101 target_ref = _form['target_ref']
1085 commit_ids = _form['revisions'][::-1]
1102 commit_ids = _form['revisions'][::-1]
1086 common_ancestor_id = _form['common_ancestor']
1103 common_ancestor_id = _form['common_ancestor']
1087
1104
1088 # find the ancestor for this pr
1105 # find the ancestor for this pr
1089 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1106 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1090 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1107 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1091
1108
1092 if not (source_db_repo or target_db_repo):
1109 if not (source_db_repo or target_db_repo):
1093 h.flash(_('source_repo or target repo not found'), category='error')
1110 h.flash(_('source_repo or target repo not found'), category='error')
1094 raise HTTPFound(
1111 raise HTTPFound(
1095 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1112 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1096
1113
1097 # re-check permissions again here
1114 # re-check permissions again here
1098 # source_repo we must have read permissions
1115 # source_repo we must have read permissions
1099
1116
1100 source_perm = HasRepoPermissionAny(
1117 source_perm = HasRepoPermissionAny(
1101 'repository.read', 'repository.write', 'repository.admin')(
1118 'repository.read', 'repository.write', 'repository.admin')(
1102 source_db_repo.repo_name)
1119 source_db_repo.repo_name)
1103 if not source_perm:
1120 if not source_perm:
1104 msg = _('Not Enough permissions to source repo `{}`.'.format(
1121 msg = _('Not Enough permissions to source repo `{}`.'.format(
1105 source_db_repo.repo_name))
1122 source_db_repo.repo_name))
1106 h.flash(msg, category='error')
1123 h.flash(msg, category='error')
1107 # copy the args back to redirect
1124 # copy the args back to redirect
1108 org_query = self.request.GET.mixed()
1125 org_query = self.request.GET.mixed()
1109 raise HTTPFound(
1126 raise HTTPFound(
1110 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1127 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1111 _query=org_query))
1128 _query=org_query))
1112
1129
1113 # target repo we must have read permissions, and also later on
1130 # target repo we must have read permissions, and also later on
1114 # we want to check branch permissions here
1131 # we want to check branch permissions here
1115 target_perm = HasRepoPermissionAny(
1132 target_perm = HasRepoPermissionAny(
1116 'repository.read', 'repository.write', 'repository.admin')(
1133 'repository.read', 'repository.write', 'repository.admin')(
1117 target_db_repo.repo_name)
1134 target_db_repo.repo_name)
1118 if not target_perm:
1135 if not target_perm:
1119 msg = _('Not Enough permissions to target repo `{}`.'.format(
1136 msg = _('Not Enough permissions to target repo `{}`.'.format(
1120 target_db_repo.repo_name))
1137 target_db_repo.repo_name))
1121 h.flash(msg, category='error')
1138 h.flash(msg, category='error')
1122 # copy the args back to redirect
1139 # copy the args back to redirect
1123 org_query = self.request.GET.mixed()
1140 org_query = self.request.GET.mixed()
1124 raise HTTPFound(
1141 raise HTTPFound(
1125 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1142 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1126 _query=org_query))
1143 _query=org_query))
1127
1144
1128 source_scm = source_db_repo.scm_instance()
1145 source_scm = source_db_repo.scm_instance()
1129 target_scm = target_db_repo.scm_instance()
1146 target_scm = target_db_repo.scm_instance()
1130
1147
1131 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1148 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1132 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1149 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1133
1150
1134 ancestor = source_scm.get_common_ancestor(
1151 ancestor = source_scm.get_common_ancestor(
1135 source_commit.raw_id, target_commit.raw_id, target_scm)
1152 source_commit.raw_id, target_commit.raw_id, target_scm)
1136
1153
1137 # recalculate target ref based on ancestor
1154 # recalculate target ref based on ancestor
1138 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1155 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1139 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1156 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1140
1157
1141 get_default_reviewers_data, validate_default_reviewers = \
1158 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1142 PullRequestModel().get_reviewer_functions()
1159 PullRequestModel().get_reviewer_functions()
1143
1160
1144 # recalculate reviewers logic, to make sure we can validate this
1161 # recalculate reviewers logic, to make sure we can validate this
1145 reviewer_rules = get_default_reviewers_data(
1162 reviewer_rules = get_default_reviewers_data(
1146 self._rhodecode_db_user, source_db_repo,
1163 self._rhodecode_db_user, source_db_repo,
1147 source_commit, target_db_repo, target_commit)
1164 source_commit, target_db_repo, target_commit)
1148
1165
1149 given_reviewers = _form['review_members']
1166 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1150 reviewers = validate_default_reviewers(
1167 observers = validate_observers(_form['observer_members'], reviewer_rules)
1151 given_reviewers, reviewer_rules)
1152
1168
1153 pullrequest_title = _form['pullrequest_title']
1169 pullrequest_title = _form['pullrequest_title']
1154 title_source_ref = source_ref.split(':', 2)[1]
1170 title_source_ref = source_ref.split(':', 2)[1]
1155 if not pullrequest_title:
1171 if not pullrequest_title:
1156 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1172 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1157 source=source_repo,
1173 source=source_repo,
1158 source_ref=title_source_ref,
1174 source_ref=title_source_ref,
1159 target=target_repo
1175 target=target_repo
1160 )
1176 )
1161
1177
1162 description = _form['pullrequest_desc']
1178 description = _form['pullrequest_desc']
1163 description_renderer = _form['description_renderer']
1179 description_renderer = _form['description_renderer']
1164
1180
1165 try:
1181 try:
1166 pull_request = PullRequestModel().create(
1182 pull_request = PullRequestModel().create(
1167 created_by=self._rhodecode_user.user_id,
1183 created_by=self._rhodecode_user.user_id,
1168 source_repo=source_repo,
1184 source_repo=source_repo,
1169 source_ref=source_ref,
1185 source_ref=source_ref,
1170 target_repo=target_repo,
1186 target_repo=target_repo,
1171 target_ref=target_ref,
1187 target_ref=target_ref,
1172 revisions=commit_ids,
1188 revisions=commit_ids,
1173 common_ancestor_id=common_ancestor_id,
1189 common_ancestor_id=common_ancestor_id,
1174 reviewers=reviewers,
1190 reviewers=reviewers,
1191 observers=observers,
1175 title=pullrequest_title,
1192 title=pullrequest_title,
1176 description=description,
1193 description=description,
1177 description_renderer=description_renderer,
1194 description_renderer=description_renderer,
1178 reviewer_data=reviewer_rules,
1195 reviewer_data=reviewer_rules,
1179 auth_user=self._rhodecode_user
1196 auth_user=self._rhodecode_user
1180 )
1197 )
1181 Session().commit()
1198 Session().commit()
1182
1199
1183 h.flash(_('Successfully opened new pull request'),
1200 h.flash(_('Successfully opened new pull request'),
1184 category='success')
1201 category='success')
1185 except Exception:
1202 except Exception:
1186 msg = _('Error occurred during creation of this pull request.')
1203 msg = _('Error occurred during creation of this pull request.')
1187 log.exception(msg)
1204 log.exception(msg)
1188 h.flash(msg, category='error')
1205 h.flash(msg, category='error')
1189
1206
1190 # copy the args back to redirect
1207 # copy the args back to redirect
1191 org_query = self.request.GET.mixed()
1208 org_query = self.request.GET.mixed()
1192 raise HTTPFound(
1209 raise HTTPFound(
1193 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1210 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1194 _query=org_query))
1211 _query=org_query))
1195
1212
1196 raise HTTPFound(
1213 raise HTTPFound(
1197 h.route_path('pullrequest_show', repo_name=target_repo,
1214 h.route_path('pullrequest_show', repo_name=target_repo,
1198 pull_request_id=pull_request.pull_request_id))
1215 pull_request_id=pull_request.pull_request_id))
1199
1216
1200 @LoginRequired()
1217 @LoginRequired()
1201 @NotAnonymous()
1218 @NotAnonymous()
1202 @HasRepoPermissionAnyDecorator(
1219 @HasRepoPermissionAnyDecorator(
1203 'repository.read', 'repository.write', 'repository.admin')
1220 'repository.read', 'repository.write', 'repository.admin')
1204 @CSRFRequired()
1221 @CSRFRequired()
1205 @view_config(
1222 @view_config(
1206 route_name='pullrequest_update', request_method='POST',
1223 route_name='pullrequest_update', request_method='POST',
1207 renderer='json_ext')
1224 renderer='json_ext')
1208 def pull_request_update(self):
1225 def pull_request_update(self):
1209 pull_request = PullRequest.get_or_404(
1226 pull_request = PullRequest.get_or_404(
1210 self.request.matchdict['pull_request_id'])
1227 self.request.matchdict['pull_request_id'])
1211 _ = self.request.translate
1228 _ = self.request.translate
1212
1229
1213 c = self.load_default_context()
1230 c = self.load_default_context()
1214 redirect_url = None
1231 redirect_url = None
1215
1232
1216 if pull_request.is_closed():
1233 if pull_request.is_closed():
1217 log.debug('update: forbidden because pull request is closed')
1234 log.debug('update: forbidden because pull request is closed')
1218 msg = _(u'Cannot update closed pull requests.')
1235 msg = _(u'Cannot update closed pull requests.')
1219 h.flash(msg, category='error')
1236 h.flash(msg, category='error')
1220 return {'response': True,
1237 return {'response': True,
1221 'redirect_url': redirect_url}
1238 'redirect_url': redirect_url}
1222
1239
1223 is_state_changing = pull_request.is_state_changing()
1240 is_state_changing = pull_request.is_state_changing()
1224 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1241 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1225 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1242 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1226
1243
1227 # only owner or admin can update it
1244 # only owner or admin can update it
1228 allowed_to_update = PullRequestModel().check_user_update(
1245 allowed_to_update = PullRequestModel().check_user_update(
1229 pull_request, self._rhodecode_user)
1246 pull_request, self._rhodecode_user)
1247
1230 if allowed_to_update:
1248 if allowed_to_update:
1231 controls = peppercorn.parse(self.request.POST.items())
1249 controls = peppercorn.parse(self.request.POST.items())
1232 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1250 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1233
1251
1234 if 'review_members' in controls:
1252 if 'review_members' in controls:
1235 self._update_reviewers(
1253 self._update_reviewers(
1254 c,
1236 pull_request, controls['review_members'],
1255 pull_request, controls['review_members'],
1237 pull_request.reviewer_data)
1256 pull_request.reviewer_data,
1257 PullRequestReviewers.ROLE_REVIEWER)
1258 elif 'observer_members' in controls:
1259 self._update_reviewers(
1260 c,
1261 pull_request, controls['observer_members'],
1262 pull_request.reviewer_data,
1263 PullRequestReviewers.ROLE_OBSERVER)
1238 elif str2bool(self.request.POST.get('update_commits', 'false')):
1264 elif str2bool(self.request.POST.get('update_commits', 'false')):
1239 if is_state_changing:
1265 if is_state_changing:
1240 log.debug('commits update: forbidden because pull request is in state %s',
1266 log.debug('commits update: forbidden because pull request is in state %s',
1241 pull_request.pull_request_state)
1267 pull_request.pull_request_state)
1242 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1268 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1243 u'Current state is: `{}`').format(
1269 u'Current state is: `{}`').format(
1244 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1270 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1245 h.flash(msg, category='error')
1271 h.flash(msg, category='error')
1246 return {'response': True,
1272 return {'response': True,
1247 'redirect_url': redirect_url}
1273 'redirect_url': redirect_url}
1248
1274
1249 self._update_commits(c, pull_request)
1275 self._update_commits(c, pull_request)
1250 if force_refresh:
1276 if force_refresh:
1251 redirect_url = h.route_path(
1277 redirect_url = h.route_path(
1252 'pullrequest_show', repo_name=self.db_repo_name,
1278 'pullrequest_show', repo_name=self.db_repo_name,
1253 pull_request_id=pull_request.pull_request_id,
1279 pull_request_id=pull_request.pull_request_id,
1254 _query={"force_refresh": 1})
1280 _query={"force_refresh": 1})
1255 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1281 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1256 self._edit_pull_request(pull_request)
1282 self._edit_pull_request(pull_request)
1257 else:
1283 else:
1284 log.error('Unhandled update data.')
1258 raise HTTPBadRequest()
1285 raise HTTPBadRequest()
1259
1286
1260 return {'response': True,
1287 return {'response': True,
1261 'redirect_url': redirect_url}
1288 'redirect_url': redirect_url}
1262 raise HTTPForbidden()
1289 raise HTTPForbidden()
1263
1290
1264 def _edit_pull_request(self, pull_request):
1291 def _edit_pull_request(self, pull_request):
1292 """
1293 Edit title and description
1294 """
1265 _ = self.request.translate
1295 _ = self.request.translate
1266
1296
1267 try:
1297 try:
1268 PullRequestModel().edit(
1298 PullRequestModel().edit(
1269 pull_request,
1299 pull_request,
1270 self.request.POST.get('title'),
1300 self.request.POST.get('title'),
1271 self.request.POST.get('description'),
1301 self.request.POST.get('description'),
1272 self.request.POST.get('description_renderer'),
1302 self.request.POST.get('description_renderer'),
1273 self._rhodecode_user)
1303 self._rhodecode_user)
1274 except ValueError:
1304 except ValueError:
1275 msg = _(u'Cannot update closed pull requests.')
1305 msg = _(u'Cannot update closed pull requests.')
1276 h.flash(msg, category='error')
1306 h.flash(msg, category='error')
1277 return
1307 return
1278 else:
1308 else:
1279 Session().commit()
1309 Session().commit()
1280
1310
1281 msg = _(u'Pull request title & description updated.')
1311 msg = _(u'Pull request title & description updated.')
1282 h.flash(msg, category='success')
1312 h.flash(msg, category='success')
1283 return
1313 return
1284
1314
1285 def _update_commits(self, c, pull_request):
1315 def _update_commits(self, c, pull_request):
1286 _ = self.request.translate
1316 _ = self.request.translate
1287
1317
1288 with pull_request.set_state(PullRequest.STATE_UPDATING):
1318 with pull_request.set_state(PullRequest.STATE_UPDATING):
1289 resp = PullRequestModel().update_commits(
1319 resp = PullRequestModel().update_commits(
1290 pull_request, self._rhodecode_db_user)
1320 pull_request, self._rhodecode_db_user)
1291
1321
1292 if resp.executed:
1322 if resp.executed:
1293
1323
1294 if resp.target_changed and resp.source_changed:
1324 if resp.target_changed and resp.source_changed:
1295 changed = 'target and source repositories'
1325 changed = 'target and source repositories'
1296 elif resp.target_changed and not resp.source_changed:
1326 elif resp.target_changed and not resp.source_changed:
1297 changed = 'target repository'
1327 changed = 'target repository'
1298 elif not resp.target_changed and resp.source_changed:
1328 elif not resp.target_changed and resp.source_changed:
1299 changed = 'source repository'
1329 changed = 'source repository'
1300 else:
1330 else:
1301 changed = 'nothing'
1331 changed = 'nothing'
1302
1332
1303 msg = _(u'Pull request updated to "{source_commit_id}" with '
1333 msg = _(u'Pull request updated to "{source_commit_id}" with '
1304 u'{count_added} added, {count_removed} removed commits. '
1334 u'{count_added} added, {count_removed} removed commits. '
1305 u'Source of changes: {change_source}')
1335 u'Source of changes: {change_source}.')
1306 msg = msg.format(
1336 msg = msg.format(
1307 source_commit_id=pull_request.source_ref_parts.commit_id,
1337 source_commit_id=pull_request.source_ref_parts.commit_id,
1308 count_added=len(resp.changes.added),
1338 count_added=len(resp.changes.added),
1309 count_removed=len(resp.changes.removed),
1339 count_removed=len(resp.changes.removed),
1310 change_source=changed)
1340 change_source=changed)
1311 h.flash(msg, category='success')
1341 h.flash(msg, category='success')
1312
1342 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1313 message = msg + (
1314 ' - <a onclick="window.location.reload()">'
1315 '<strong>{}</strong></a>'.format(_('Reload page')))
1316
1317 message_obj = {
1318 'message': message,
1319 'level': 'success',
1320 'topic': '/notifications'
1321 }
1322
1323 channelstream.post_message(
1324 c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
1325 registry=self.request.registry)
1326 else:
1343 else:
1327 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1344 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1328 warning_reasons = [
1345 warning_reasons = [
1329 UpdateFailureReason.NO_CHANGE,
1346 UpdateFailureReason.NO_CHANGE,
1330 UpdateFailureReason.WRONG_REF_TYPE,
1347 UpdateFailureReason.WRONG_REF_TYPE,
1331 ]
1348 ]
1332 category = 'warning' if resp.reason in warning_reasons else 'error'
1349 category = 'warning' if resp.reason in warning_reasons else 'error'
1333 h.flash(msg, category=category)
1350 h.flash(msg, category=category)
1334
1351
1352 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1353 _ = self.request.translate
1354
1355 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1356 PullRequestModel().get_reviewer_functions()
1357
1358 if role == PullRequestReviewers.ROLE_REVIEWER:
1359 try:
1360 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1361 except ValueError as e:
1362 log.error('Reviewers Validation: {}'.format(e))
1363 h.flash(e, category='error')
1364 return
1365
1366 old_calculated_status = pull_request.calculated_review_status()
1367 PullRequestModel().update_reviewers(
1368 pull_request, reviewers, self._rhodecode_user)
1369
1370 Session().commit()
1371
1372 msg = _('Pull request reviewers updated.')
1373 h.flash(msg, category='success')
1374 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1375
1376 # trigger status changed if change in reviewers changes the status
1377 calculated_status = pull_request.calculated_review_status()
1378 if old_calculated_status != calculated_status:
1379 PullRequestModel().trigger_pull_request_hook(
1380 pull_request, self._rhodecode_user, 'review_status_change',
1381 data={'status': calculated_status})
1382
1383 elif role == PullRequestReviewers.ROLE_OBSERVER:
1384 try:
1385 observers = validate_observers(review_members, reviewer_rules)
1386 except ValueError as e:
1387 log.error('Observers Validation: {}'.format(e))
1388 h.flash(e, category='error')
1389 return
1390
1391 PullRequestModel().update_observers(
1392 pull_request, observers, self._rhodecode_user)
1393
1394 Session().commit()
1395 msg = _('Pull request observers updated.')
1396 h.flash(msg, category='success')
1397 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1398
1335 @LoginRequired()
1399 @LoginRequired()
1336 @NotAnonymous()
1400 @NotAnonymous()
1337 @HasRepoPermissionAnyDecorator(
1401 @HasRepoPermissionAnyDecorator(
1338 'repository.read', 'repository.write', 'repository.admin')
1402 'repository.read', 'repository.write', 'repository.admin')
1339 @CSRFRequired()
1403 @CSRFRequired()
1340 @view_config(
1404 @view_config(
1341 route_name='pullrequest_merge', request_method='POST',
1405 route_name='pullrequest_merge', request_method='POST',
1342 renderer='json_ext')
1406 renderer='json_ext')
1343 def pull_request_merge(self):
1407 def pull_request_merge(self):
1344 """
1408 """
1345 Merge will perform a server-side merge of the specified
1409 Merge will perform a server-side merge of the specified
1346 pull request, if the pull request is approved and mergeable.
1410 pull request, if the pull request is approved and mergeable.
1347 After successful merging, the pull request is automatically
1411 After successful merging, the pull request is automatically
1348 closed, with a relevant comment.
1412 closed, with a relevant comment.
1349 """
1413 """
1350 pull_request = PullRequest.get_or_404(
1414 pull_request = PullRequest.get_or_404(
1351 self.request.matchdict['pull_request_id'])
1415 self.request.matchdict['pull_request_id'])
1352 _ = self.request.translate
1416 _ = self.request.translate
1353
1417
1354 if pull_request.is_state_changing():
1418 if pull_request.is_state_changing():
1355 log.debug('show: forbidden because pull request is in state %s',
1419 log.debug('show: forbidden because pull request is in state %s',
1356 pull_request.pull_request_state)
1420 pull_request.pull_request_state)
1357 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1421 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1358 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1422 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1359 pull_request.pull_request_state)
1423 pull_request.pull_request_state)
1360 h.flash(msg, category='error')
1424 h.flash(msg, category='error')
1361 raise HTTPFound(
1425 raise HTTPFound(
1362 h.route_path('pullrequest_show',
1426 h.route_path('pullrequest_show',
1363 repo_name=pull_request.target_repo.repo_name,
1427 repo_name=pull_request.target_repo.repo_name,
1364 pull_request_id=pull_request.pull_request_id))
1428 pull_request_id=pull_request.pull_request_id))
1365
1429
1366 self.load_default_context()
1430 self.load_default_context()
1367
1431
1368 with pull_request.set_state(PullRequest.STATE_UPDATING):
1432 with pull_request.set_state(PullRequest.STATE_UPDATING):
1369 check = MergeCheck.validate(
1433 check = MergeCheck.validate(
1370 pull_request, auth_user=self._rhodecode_user,
1434 pull_request, auth_user=self._rhodecode_user,
1371 translator=self.request.translate)
1435 translator=self.request.translate)
1372 merge_possible = not check.failed
1436 merge_possible = not check.failed
1373
1437
1374 for err_type, error_msg in check.errors:
1438 for err_type, error_msg in check.errors:
1375 h.flash(error_msg, category=err_type)
1439 h.flash(error_msg, category=err_type)
1376
1440
1377 if merge_possible:
1441 if merge_possible:
1378 log.debug("Pre-conditions checked, trying to merge.")
1442 log.debug("Pre-conditions checked, trying to merge.")
1379 extras = vcs_operation_context(
1443 extras = vcs_operation_context(
1380 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1444 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1381 username=self._rhodecode_db_user.username, action='push',
1445 username=self._rhodecode_db_user.username, action='push',
1382 scm=pull_request.target_repo.repo_type)
1446 scm=pull_request.target_repo.repo_type)
1383 with pull_request.set_state(PullRequest.STATE_UPDATING):
1447 with pull_request.set_state(PullRequest.STATE_UPDATING):
1384 self._merge_pull_request(
1448 self._merge_pull_request(
1385 pull_request, self._rhodecode_db_user, extras)
1449 pull_request, self._rhodecode_db_user, extras)
1386 else:
1450 else:
1387 log.debug("Pre-conditions failed, NOT merging.")
1451 log.debug("Pre-conditions failed, NOT merging.")
1388
1452
1389 raise HTTPFound(
1453 raise HTTPFound(
1390 h.route_path('pullrequest_show',
1454 h.route_path('pullrequest_show',
1391 repo_name=pull_request.target_repo.repo_name,
1455 repo_name=pull_request.target_repo.repo_name,
1392 pull_request_id=pull_request.pull_request_id))
1456 pull_request_id=pull_request.pull_request_id))
1393
1457
1394 def _merge_pull_request(self, pull_request, user, extras):
1458 def _merge_pull_request(self, pull_request, user, extras):
1395 _ = self.request.translate
1459 _ = self.request.translate
1396 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1460 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1397
1461
1398 if merge_resp.executed:
1462 if merge_resp.executed:
1399 log.debug("The merge was successful, closing the pull request.")
1463 log.debug("The merge was successful, closing the pull request.")
1400 PullRequestModel().close_pull_request(
1464 PullRequestModel().close_pull_request(
1401 pull_request.pull_request_id, user)
1465 pull_request.pull_request_id, user)
1402 Session().commit()
1466 Session().commit()
1403 msg = _('Pull request was successfully merged and closed.')
1467 msg = _('Pull request was successfully merged and closed.')
1404 h.flash(msg, category='success')
1468 h.flash(msg, category='success')
1405 else:
1469 else:
1406 log.debug(
1470 log.debug(
1407 "The merge was not successful. Merge response: %s", merge_resp)
1471 "The merge was not successful. Merge response: %s", merge_resp)
1408 msg = merge_resp.merge_status_message
1472 msg = merge_resp.merge_status_message
1409 h.flash(msg, category='error')
1473 h.flash(msg, category='error')
1410
1474
1411 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1412 _ = self.request.translate
1413
1414 get_default_reviewers_data, validate_default_reviewers = \
1415 PullRequestModel().get_reviewer_functions()
1416
1417 try:
1418 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1419 except ValueError as e:
1420 log.error('Reviewers Validation: {}'.format(e))
1421 h.flash(e, category='error')
1422 return
1423
1424 old_calculated_status = pull_request.calculated_review_status()
1425 PullRequestModel().update_reviewers(
1426 pull_request, reviewers, self._rhodecode_user)
1427 h.flash(_('Pull request reviewers updated.'), category='success')
1428 Session().commit()
1429
1430 # trigger status changed if change in reviewers changes the status
1431 calculated_status = pull_request.calculated_review_status()
1432 if old_calculated_status != calculated_status:
1433 PullRequestModel().trigger_pull_request_hook(
1434 pull_request, self._rhodecode_user, 'review_status_change',
1435 data={'status': calculated_status})
1436
1437 @LoginRequired()
1475 @LoginRequired()
1438 @NotAnonymous()
1476 @NotAnonymous()
1439 @HasRepoPermissionAnyDecorator(
1477 @HasRepoPermissionAnyDecorator(
1440 'repository.read', 'repository.write', 'repository.admin')
1478 'repository.read', 'repository.write', 'repository.admin')
1441 @CSRFRequired()
1479 @CSRFRequired()
1442 @view_config(
1480 @view_config(
1443 route_name='pullrequest_delete', request_method='POST',
1481 route_name='pullrequest_delete', request_method='POST',
1444 renderer='json_ext')
1482 renderer='json_ext')
1445 def pull_request_delete(self):
1483 def pull_request_delete(self):
1446 _ = self.request.translate
1484 _ = self.request.translate
1447
1485
1448 pull_request = PullRequest.get_or_404(
1486 pull_request = PullRequest.get_or_404(
1449 self.request.matchdict['pull_request_id'])
1487 self.request.matchdict['pull_request_id'])
1450 self.load_default_context()
1488 self.load_default_context()
1451
1489
1452 pr_closed = pull_request.is_closed()
1490 pr_closed = pull_request.is_closed()
1453 allowed_to_delete = PullRequestModel().check_user_delete(
1491 allowed_to_delete = PullRequestModel().check_user_delete(
1454 pull_request, self._rhodecode_user) and not pr_closed
1492 pull_request, self._rhodecode_user) and not pr_closed
1455
1493
1456 # only owner can delete it !
1494 # only owner can delete it !
1457 if allowed_to_delete:
1495 if allowed_to_delete:
1458 PullRequestModel().delete(pull_request, self._rhodecode_user)
1496 PullRequestModel().delete(pull_request, self._rhodecode_user)
1459 Session().commit()
1497 Session().commit()
1460 h.flash(_('Successfully deleted pull request'),
1498 h.flash(_('Successfully deleted pull request'),
1461 category='success')
1499 category='success')
1462 raise HTTPFound(h.route_path('pullrequest_show_all',
1500 raise HTTPFound(h.route_path('pullrequest_show_all',
1463 repo_name=self.db_repo_name))
1501 repo_name=self.db_repo_name))
1464
1502
1465 log.warning('user %s tried to delete pull request without access',
1503 log.warning('user %s tried to delete pull request without access',
1466 self._rhodecode_user)
1504 self._rhodecode_user)
1467 raise HTTPNotFound()
1505 raise HTTPNotFound()
1468
1506
1469 @LoginRequired()
1507 @LoginRequired()
1470 @NotAnonymous()
1508 @NotAnonymous()
1471 @HasRepoPermissionAnyDecorator(
1509 @HasRepoPermissionAnyDecorator(
1472 'repository.read', 'repository.write', 'repository.admin')
1510 'repository.read', 'repository.write', 'repository.admin')
1473 @CSRFRequired()
1511 @CSRFRequired()
1474 @view_config(
1512 @view_config(
1475 route_name='pullrequest_comment_create', request_method='POST',
1513 route_name='pullrequest_comment_create', request_method='POST',
1476 renderer='json_ext')
1514 renderer='json_ext')
1477 def pull_request_comment_create(self):
1515 def pull_request_comment_create(self):
1478 _ = self.request.translate
1516 _ = self.request.translate
1479
1517
1480 pull_request = PullRequest.get_or_404(
1518 pull_request = PullRequest.get_or_404(
1481 self.request.matchdict['pull_request_id'])
1519 self.request.matchdict['pull_request_id'])
1482 pull_request_id = pull_request.pull_request_id
1520 pull_request_id = pull_request.pull_request_id
1483
1521
1484 if pull_request.is_closed():
1522 if pull_request.is_closed():
1485 log.debug('comment: forbidden because pull request is closed')
1523 log.debug('comment: forbidden because pull request is closed')
1486 raise HTTPForbidden()
1524 raise HTTPForbidden()
1487
1525
1488 allowed_to_comment = PullRequestModel().check_user_comment(
1526 allowed_to_comment = PullRequestModel().check_user_comment(
1489 pull_request, self._rhodecode_user)
1527 pull_request, self._rhodecode_user)
1490 if not allowed_to_comment:
1528 if not allowed_to_comment:
1491 log.debug(
1529 log.debug('comment: forbidden because pull request is from forbidden repo')
1492 'comment: forbidden because pull request is from forbidden repo')
1493 raise HTTPForbidden()
1530 raise HTTPForbidden()
1494
1531
1495 c = self.load_default_context()
1532 c = self.load_default_context()
1496
1533
1497 status = self.request.POST.get('changeset_status', None)
1534 status = self.request.POST.get('changeset_status', None)
1498 text = self.request.POST.get('text')
1535 text = self.request.POST.get('text')
1499 comment_type = self.request.POST.get('comment_type')
1536 comment_type = self.request.POST.get('comment_type')
1500 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1537 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1501 close_pull_request = self.request.POST.get('close_pull_request')
1538 close_pull_request = self.request.POST.get('close_pull_request')
1502
1539
1503 # the logic here should work like following, if we submit close
1540 # the logic here should work like following, if we submit close
1504 # pr comment, use `close_pull_request_with_comment` function
1541 # pr comment, use `close_pull_request_with_comment` function
1505 # else handle regular comment logic
1542 # else handle regular comment logic
1506
1543
1507 if close_pull_request:
1544 if close_pull_request:
1508 # only owner or admin or person with write permissions
1545 # only owner or admin or person with write permissions
1509 allowed_to_close = PullRequestModel().check_user_update(
1546 allowed_to_close = PullRequestModel().check_user_update(
1510 pull_request, self._rhodecode_user)
1547 pull_request, self._rhodecode_user)
1511 if not allowed_to_close:
1548 if not allowed_to_close:
1512 log.debug('comment: forbidden because not allowed to close '
1549 log.debug('comment: forbidden because not allowed to close '
1513 'pull request %s', pull_request_id)
1550 'pull request %s', pull_request_id)
1514 raise HTTPForbidden()
1551 raise HTTPForbidden()
1515
1552
1516 # This also triggers `review_status_change`
1553 # This also triggers `review_status_change`
1517 comment, status = PullRequestModel().close_pull_request_with_comment(
1554 comment, status = PullRequestModel().close_pull_request_with_comment(
1518 pull_request, self._rhodecode_user, self.db_repo, message=text,
1555 pull_request, self._rhodecode_user, self.db_repo, message=text,
1519 auth_user=self._rhodecode_user)
1556 auth_user=self._rhodecode_user)
1520 Session().flush()
1557 Session().flush()
1521
1558
1522 PullRequestModel().trigger_pull_request_hook(
1559 PullRequestModel().trigger_pull_request_hook(
1523 pull_request, self._rhodecode_user, 'comment',
1560 pull_request, self._rhodecode_user, 'comment',
1524 data={'comment': comment})
1561 data={'comment': comment})
1525
1562
1526 else:
1563 else:
1527 # regular comment case, could be inline, or one with status.
1564 # regular comment case, could be inline, or one with status.
1528 # for that one we check also permissions
1565 # for that one we check also permissions
1529
1566
1530 allowed_to_change_status = PullRequestModel().check_user_change_status(
1567 allowed_to_change_status = PullRequestModel().check_user_change_status(
1531 pull_request, self._rhodecode_user)
1568 pull_request, self._rhodecode_user)
1532
1569
1533 if status and allowed_to_change_status:
1570 if status and allowed_to_change_status:
1534 message = (_('Status change %(transition_icon)s %(status)s')
1571 message = (_('Status change %(transition_icon)s %(status)s')
1535 % {'transition_icon': '>',
1572 % {'transition_icon': '>',
1536 'status': ChangesetStatus.get_status_lbl(status)})
1573 'status': ChangesetStatus.get_status_lbl(status)})
1537 text = text or message
1574 text = text or message
1538
1575
1539 comment = CommentsModel().create(
1576 comment = CommentsModel().create(
1540 text=text,
1577 text=text,
1541 repo=self.db_repo.repo_id,
1578 repo=self.db_repo.repo_id,
1542 user=self._rhodecode_user.user_id,
1579 user=self._rhodecode_user.user_id,
1543 pull_request=pull_request,
1580 pull_request=pull_request,
1544 f_path=self.request.POST.get('f_path'),
1581 f_path=self.request.POST.get('f_path'),
1545 line_no=self.request.POST.get('line'),
1582 line_no=self.request.POST.get('line'),
1546 status_change=(ChangesetStatus.get_status_lbl(status)
1583 status_change=(ChangesetStatus.get_status_lbl(status)
1547 if status and allowed_to_change_status else None),
1584 if status and allowed_to_change_status else None),
1548 status_change_type=(status
1585 status_change_type=(status
1549 if status and allowed_to_change_status else None),
1586 if status and allowed_to_change_status else None),
1550 comment_type=comment_type,
1587 comment_type=comment_type,
1551 resolves_comment_id=resolves_comment_id,
1588 resolves_comment_id=resolves_comment_id,
1552 auth_user=self._rhodecode_user
1589 auth_user=self._rhodecode_user
1553 )
1590 )
1554
1591
1555 if allowed_to_change_status:
1592 if allowed_to_change_status:
1556 # calculate old status before we change it
1593 # calculate old status before we change it
1557 old_calculated_status = pull_request.calculated_review_status()
1594 old_calculated_status = pull_request.calculated_review_status()
1558
1595
1559 # get status if set !
1596 # get status if set !
1560 if status:
1597 if status:
1561 ChangesetStatusModel().set_status(
1598 ChangesetStatusModel().set_status(
1562 self.db_repo.repo_id,
1599 self.db_repo.repo_id,
1563 status,
1600 status,
1564 self._rhodecode_user.user_id,
1601 self._rhodecode_user.user_id,
1565 comment,
1602 comment,
1566 pull_request=pull_request
1603 pull_request=pull_request
1567 )
1604 )
1568
1605
1569 Session().flush()
1606 Session().flush()
1570 # this is somehow required to get access to some relationship
1607 # this is somehow required to get access to some relationship
1571 # loaded on comment
1608 # loaded on comment
1572 Session().refresh(comment)
1609 Session().refresh(comment)
1573
1610
1574 PullRequestModel().trigger_pull_request_hook(
1611 PullRequestModel().trigger_pull_request_hook(
1575 pull_request, self._rhodecode_user, 'comment',
1612 pull_request, self._rhodecode_user, 'comment',
1576 data={'comment': comment})
1613 data={'comment': comment})
1577
1614
1578 # we now calculate the status of pull request, and based on that
1615 # we now calculate the status of pull request, and based on that
1579 # calculation we set the commits status
1616 # calculation we set the commits status
1580 calculated_status = pull_request.calculated_review_status()
1617 calculated_status = pull_request.calculated_review_status()
1581 if old_calculated_status != calculated_status:
1618 if old_calculated_status != calculated_status:
1582 PullRequestModel().trigger_pull_request_hook(
1619 PullRequestModel().trigger_pull_request_hook(
1583 pull_request, self._rhodecode_user, 'review_status_change',
1620 pull_request, self._rhodecode_user, 'review_status_change',
1584 data={'status': calculated_status})
1621 data={'status': calculated_status})
1585
1622
1586 Session().commit()
1623 Session().commit()
1587
1624
1588 data = {
1625 data = {
1589 'target_id': h.safeid(h.safe_unicode(
1626 'target_id': h.safeid(h.safe_unicode(
1590 self.request.POST.get('f_path'))),
1627 self.request.POST.get('f_path'))),
1591 }
1628 }
1592 if comment:
1629 if comment:
1593 c.co = comment
1630 c.co = comment
1594 c.at_version_num = None
1631 c.at_version_num = None
1595 rendered_comment = render(
1632 rendered_comment = render(
1596 'rhodecode:templates/changeset/changeset_comment_block.mako',
1633 'rhodecode:templates/changeset/changeset_comment_block.mako',
1597 self._get_template_context(c), self.request)
1634 self._get_template_context(c), self.request)
1598
1635
1599 data.update(comment.get_dict())
1636 data.update(comment.get_dict())
1600 data.update({'rendered_text': rendered_comment})
1637 data.update({'rendered_text': rendered_comment})
1601
1638
1602 return data
1639 return data
1603
1640
1604 @LoginRequired()
1641 @LoginRequired()
1605 @NotAnonymous()
1642 @NotAnonymous()
1606 @HasRepoPermissionAnyDecorator(
1643 @HasRepoPermissionAnyDecorator(
1607 'repository.read', 'repository.write', 'repository.admin')
1644 'repository.read', 'repository.write', 'repository.admin')
1608 @CSRFRequired()
1645 @CSRFRequired()
1609 @view_config(
1646 @view_config(
1610 route_name='pullrequest_comment_delete', request_method='POST',
1647 route_name='pullrequest_comment_delete', request_method='POST',
1611 renderer='json_ext')
1648 renderer='json_ext')
1612 def pull_request_comment_delete(self):
1649 def pull_request_comment_delete(self):
1613 pull_request = PullRequest.get_or_404(
1650 pull_request = PullRequest.get_or_404(
1614 self.request.matchdict['pull_request_id'])
1651 self.request.matchdict['pull_request_id'])
1615
1652
1616 comment = ChangesetComment.get_or_404(
1653 comment = ChangesetComment.get_or_404(
1617 self.request.matchdict['comment_id'])
1654 self.request.matchdict['comment_id'])
1618 comment_id = comment.comment_id
1655 comment_id = comment.comment_id
1619
1656
1620 if comment.immutable:
1657 if comment.immutable:
1621 # don't allow deleting comments that are immutable
1658 # don't allow deleting comments that are immutable
1622 raise HTTPForbidden()
1659 raise HTTPForbidden()
1623
1660
1624 if pull_request.is_closed():
1661 if pull_request.is_closed():
1625 log.debug('comment: forbidden because pull request is closed')
1662 log.debug('comment: forbidden because pull request is closed')
1626 raise HTTPForbidden()
1663 raise HTTPForbidden()
1627
1664
1628 if not comment:
1665 if not comment:
1629 log.debug('Comment with id:%s not found, skipping', comment_id)
1666 log.debug('Comment with id:%s not found, skipping', comment_id)
1630 # comment already deleted in another call probably
1667 # comment already deleted in another call probably
1631 return True
1668 return True
1632
1669
1633 if comment.pull_request.is_closed():
1670 if comment.pull_request.is_closed():
1634 # don't allow deleting comments on closed pull request
1671 # don't allow deleting comments on closed pull request
1635 raise HTTPForbidden()
1672 raise HTTPForbidden()
1636
1673
1637 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1674 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1638 super_admin = h.HasPermissionAny('hg.admin')()
1675 super_admin = h.HasPermissionAny('hg.admin')()
1639 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1676 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1640 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1677 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1641 comment_repo_admin = is_repo_admin and is_repo_comment
1678 comment_repo_admin = is_repo_admin and is_repo_comment
1642
1679
1643 if super_admin or comment_owner or comment_repo_admin:
1680 if super_admin or comment_owner or comment_repo_admin:
1644 old_calculated_status = comment.pull_request.calculated_review_status()
1681 old_calculated_status = comment.pull_request.calculated_review_status()
1645 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1682 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1646 Session().commit()
1683 Session().commit()
1647 calculated_status = comment.pull_request.calculated_review_status()
1684 calculated_status = comment.pull_request.calculated_review_status()
1648 if old_calculated_status != calculated_status:
1685 if old_calculated_status != calculated_status:
1649 PullRequestModel().trigger_pull_request_hook(
1686 PullRequestModel().trigger_pull_request_hook(
1650 comment.pull_request, self._rhodecode_user, 'review_status_change',
1687 comment.pull_request, self._rhodecode_user, 'review_status_change',
1651 data={'status': calculated_status})
1688 data={'status': calculated_status})
1652 return True
1689 return True
1653 else:
1690 else:
1654 log.warning('No permissions for user %s to delete comment_id: %s',
1691 log.warning('No permissions for user %s to delete comment_id: %s',
1655 self._rhodecode_db_user, comment_id)
1692 self._rhodecode_db_user, comment_id)
1656 raise HTTPNotFound()
1693 raise HTTPNotFound()
1657
1694
1658 @LoginRequired()
1695 @LoginRequired()
1659 @NotAnonymous()
1696 @NotAnonymous()
1660 @HasRepoPermissionAnyDecorator(
1697 @HasRepoPermissionAnyDecorator(
1661 'repository.read', 'repository.write', 'repository.admin')
1698 'repository.read', 'repository.write', 'repository.admin')
1662 @CSRFRequired()
1699 @CSRFRequired()
1663 @view_config(
1700 @view_config(
1664 route_name='pullrequest_comment_edit', request_method='POST',
1701 route_name='pullrequest_comment_edit', request_method='POST',
1665 renderer='json_ext')
1702 renderer='json_ext')
1666 def pull_request_comment_edit(self):
1703 def pull_request_comment_edit(self):
1667 self.load_default_context()
1704 self.load_default_context()
1668
1705
1669 pull_request = PullRequest.get_or_404(
1706 pull_request = PullRequest.get_or_404(
1670 self.request.matchdict['pull_request_id']
1707 self.request.matchdict['pull_request_id']
1671 )
1708 )
1672 comment = ChangesetComment.get_or_404(
1709 comment = ChangesetComment.get_or_404(
1673 self.request.matchdict['comment_id']
1710 self.request.matchdict['comment_id']
1674 )
1711 )
1675 comment_id = comment.comment_id
1712 comment_id = comment.comment_id
1676
1713
1677 if comment.immutable:
1714 if comment.immutable:
1678 # don't allow deleting comments that are immutable
1715 # don't allow deleting comments that are immutable
1679 raise HTTPForbidden()
1716 raise HTTPForbidden()
1680
1717
1681 if pull_request.is_closed():
1718 if pull_request.is_closed():
1682 log.debug('comment: forbidden because pull request is closed')
1719 log.debug('comment: forbidden because pull request is closed')
1683 raise HTTPForbidden()
1720 raise HTTPForbidden()
1684
1721
1685 if not comment:
1722 if not comment:
1686 log.debug('Comment with id:%s not found, skipping', comment_id)
1723 log.debug('Comment with id:%s not found, skipping', comment_id)
1687 # comment already deleted in another call probably
1724 # comment already deleted in another call probably
1688 return True
1725 return True
1689
1726
1690 if comment.pull_request.is_closed():
1727 if comment.pull_request.is_closed():
1691 # don't allow deleting comments on closed pull request
1728 # don't allow deleting comments on closed pull request
1692 raise HTTPForbidden()
1729 raise HTTPForbidden()
1693
1730
1694 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1731 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1695 super_admin = h.HasPermissionAny('hg.admin')()
1732 super_admin = h.HasPermissionAny('hg.admin')()
1696 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1733 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1697 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1734 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1698 comment_repo_admin = is_repo_admin and is_repo_comment
1735 comment_repo_admin = is_repo_admin and is_repo_comment
1699
1736
1700 if super_admin or comment_owner or comment_repo_admin:
1737 if super_admin or comment_owner or comment_repo_admin:
1701 text = self.request.POST.get('text')
1738 text = self.request.POST.get('text')
1702 version = self.request.POST.get('version')
1739 version = self.request.POST.get('version')
1703 if text == comment.text:
1740 if text == comment.text:
1704 log.warning(
1741 log.warning(
1705 'Comment(PR): '
1742 'Comment(PR): '
1706 'Trying to create new version '
1743 'Trying to create new version '
1707 'with the same comment body {}'.format(
1744 'with the same comment body {}'.format(
1708 comment_id,
1745 comment_id,
1709 )
1746 )
1710 )
1747 )
1711 raise HTTPNotFound()
1748 raise HTTPNotFound()
1712
1749
1713 if version.isdigit():
1750 if version.isdigit():
1714 version = int(version)
1751 version = int(version)
1715 else:
1752 else:
1716 log.warning(
1753 log.warning(
1717 'Comment(PR): Wrong version type {} {} '
1754 'Comment(PR): Wrong version type {} {} '
1718 'for comment {}'.format(
1755 'for comment {}'.format(
1719 version,
1756 version,
1720 type(version),
1757 type(version),
1721 comment_id,
1758 comment_id,
1722 )
1759 )
1723 )
1760 )
1724 raise HTTPNotFound()
1761 raise HTTPNotFound()
1725
1762
1726 try:
1763 try:
1727 comment_history = CommentsModel().edit(
1764 comment_history = CommentsModel().edit(
1728 comment_id=comment_id,
1765 comment_id=comment_id,
1729 text=text,
1766 text=text,
1730 auth_user=self._rhodecode_user,
1767 auth_user=self._rhodecode_user,
1731 version=version,
1768 version=version,
1732 )
1769 )
1733 except CommentVersionMismatch:
1770 except CommentVersionMismatch:
1734 raise HTTPConflict()
1771 raise HTTPConflict()
1735
1772
1736 if not comment_history:
1773 if not comment_history:
1737 raise HTTPNotFound()
1774 raise HTTPNotFound()
1738
1775
1739 Session().commit()
1776 Session().commit()
1740
1777
1741 PullRequestModel().trigger_pull_request_hook(
1778 PullRequestModel().trigger_pull_request_hook(
1742 pull_request, self._rhodecode_user, 'comment_edit',
1779 pull_request, self._rhodecode_user, 'comment_edit',
1743 data={'comment': comment})
1780 data={'comment': comment})
1744
1781
1745 return {
1782 return {
1746 'comment_history_id': comment_history.comment_history_id,
1783 'comment_history_id': comment_history.comment_history_id,
1747 'comment_id': comment.comment_id,
1784 'comment_id': comment.comment_id,
1748 'comment_version': comment_history.version,
1785 'comment_version': comment_history.version,
1749 'comment_author_username': comment_history.author.username,
1786 'comment_author_username': comment_history.author.username,
1750 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1787 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1751 'comment_created_on': h.age_component(comment_history.created_on,
1788 'comment_created_on': h.age_component(comment_history.created_on,
1752 time_is_local=True),
1789 time_is_local=True),
1753 }
1790 }
1754 else:
1791 else:
1755 log.warning('No permissions for user %s to edit comment_id: %s',
1792 log.warning('No permissions for user %s to edit comment_id: %s',
1756 self._rhodecode_db_user, comment_id)
1793 self._rhodecode_db_user, comment_id)
1757 raise HTTPNotFound()
1794 raise HTTPNotFound()
@@ -1,763 +1,767 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 import os
21 import os
22 import sys
22 import sys
23 import logging
23 import logging
24 import collections
24 import collections
25 import tempfile
25 import tempfile
26 import time
26 import time
27
27
28 from paste.gzipper import make_gzip_middleware
28 from paste.gzipper import make_gzip_middleware
29 import pyramid.events
29 import pyramid.events
30 from pyramid.wsgi import wsgiapp
30 from pyramid.wsgi import wsgiapp
31 from pyramid.authorization import ACLAuthorizationPolicy
31 from pyramid.authorization import ACLAuthorizationPolicy
32 from pyramid.config import Configurator
32 from pyramid.config import Configurator
33 from pyramid.settings import asbool, aslist
33 from pyramid.settings import asbool, aslist
34 from pyramid.httpexceptions import (
34 from pyramid.httpexceptions import (
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 from pyramid.renderers import render_to_response
36 from pyramid.renderers import render_to_response
37
37
38 from rhodecode.model import meta
38 from rhodecode.model import meta
39 from rhodecode.config import patches
39 from rhodecode.config import patches
40 from rhodecode.config import utils as config_utils
40 from rhodecode.config import utils as config_utils
41 from rhodecode.config.environment import load_pyramid_environment
41 from rhodecode.config.environment import load_pyramid_environment
42
42
43 import rhodecode.events
43 import rhodecode.events
44 from rhodecode.lib.middleware.vcs import VCSMiddleware
44 from rhodecode.lib.middleware.vcs import VCSMiddleware
45 from rhodecode.lib.request import Request
45 from rhodecode.lib.request import Request
46 from rhodecode.lib.vcs import VCSCommunicationError
46 from rhodecode.lib.vcs import VCSCommunicationError
47 from rhodecode.lib.exceptions import VCSServerUnavailable
47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 from rhodecode.lib.celerylib.loader import configure_celery
50 from rhodecode.lib.celerylib.loader import configure_celery
51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
52 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
53 from rhodecode.lib.exc_tracking import store_exception
53 from rhodecode.lib.exc_tracking import store_exception
54 from rhodecode.subscribers import (
54 from rhodecode.subscribers import (
55 scan_repositories_if_enabled, write_js_routes_if_enabled,
55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 write_metadata_if_needed, write_usage_data, inject_app_settings)
56 write_metadata_if_needed, write_usage_data, inject_app_settings)
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 def is_http_error(response):
62 def is_http_error(response):
63 # error which should have traceback
63 # error which should have traceback
64 return response.status_code > 499
64 return response.status_code > 499
65
65
66
66
67 def should_load_all():
67 def should_load_all():
68 """
68 """
69 Returns if all application components should be loaded. In some cases it's
69 Returns if all application components should be loaded. In some cases it's
70 desired to skip apps loading for faster shell script execution
70 desired to skip apps loading for faster shell script execution
71 """
71 """
72 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
72 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
73 if ssh_cmd:
73 if ssh_cmd:
74 return False
74 return False
75
75
76 return True
76 return True
77
77
78
78
79 def make_pyramid_app(global_config, **settings):
79 def make_pyramid_app(global_config, **settings):
80 """
80 """
81 Constructs the WSGI application based on Pyramid.
81 Constructs the WSGI application based on Pyramid.
82
82
83 Specials:
83 Specials:
84
84
85 * The application can also be integrated like a plugin via the call to
85 * The application can also be integrated like a plugin via the call to
86 `includeme`. This is accompanied with the other utility functions which
86 `includeme`. This is accompanied with the other utility functions which
87 are called. Changing this should be done with great care to not break
87 are called. Changing this should be done with great care to not break
88 cases when these fragments are assembled from another place.
88 cases when these fragments are assembled from another place.
89
89
90 """
90 """
91
91
92 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
92 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
93 # will be replaced by the value of the environment variable "NAME" in this case.
93 # will be replaced by the value of the environment variable "NAME" in this case.
94 start_time = time.time()
94 start_time = time.time()
95
95
96 debug = asbool(global_config.get('debug'))
96 debug = asbool(global_config.get('debug'))
97 if debug:
97 if debug:
98 enable_debug()
98 enable_debug()
99
99
100 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
100 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
101
101
102 global_config = _substitute_values(global_config, environ)
102 global_config = _substitute_values(global_config, environ)
103 settings = _substitute_values(settings, environ)
103 settings = _substitute_values(settings, environ)
104
104
105 sanitize_settings_and_apply_defaults(global_config, settings)
105 sanitize_settings_and_apply_defaults(global_config, settings)
106
106
107 config = Configurator(settings=settings)
107 config = Configurator(settings=settings)
108
108
109 # Apply compatibility patches
109 # Apply compatibility patches
110 patches.inspect_getargspec()
110 patches.inspect_getargspec()
111
111
112 load_pyramid_environment(global_config, settings)
112 load_pyramid_environment(global_config, settings)
113
113
114 # Static file view comes first
114 # Static file view comes first
115 includeme_first(config)
115 includeme_first(config)
116
116
117 includeme(config)
117 includeme(config)
118
118
119 pyramid_app = config.make_wsgi_app()
119 pyramid_app = config.make_wsgi_app()
120 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
120 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
121 pyramid_app.config = config
121 pyramid_app.config = config
122
122
123 config.configure_celery(global_config['__file__'])
123 config.configure_celery(global_config['__file__'])
124 # creating the app uses a connection - return it after we are done
124 # creating the app uses a connection - return it after we are done
125 meta.Session.remove()
125 meta.Session.remove()
126 total_time = time.time() - start_time
126 total_time = time.time() - start_time
127 log.info('Pyramid app `%s` created and configured in %.2fs',
127 log.info('Pyramid app `%s` created and configured in %.2fs',
128 pyramid_app.func_name, total_time)
128 pyramid_app.func_name, total_time)
129
129
130 return pyramid_app
130 return pyramid_app
131
131
132
132
133 def not_found_view(request):
133 def not_found_view(request):
134 """
134 """
135 This creates the view which should be registered as not-found-view to
135 This creates the view which should be registered as not-found-view to
136 pyramid.
136 pyramid.
137 """
137 """
138
138
139 if not getattr(request, 'vcs_call', None):
139 if not getattr(request, 'vcs_call', None):
140 # handle like regular case with our error_handler
140 # handle like regular case with our error_handler
141 return error_handler(HTTPNotFound(), request)
141 return error_handler(HTTPNotFound(), request)
142
142
143 # handle not found view as a vcs call
143 # handle not found view as a vcs call
144 settings = request.registry.settings
144 settings = request.registry.settings
145 ae_client = getattr(request, 'ae_client', None)
145 ae_client = getattr(request, 'ae_client', None)
146 vcs_app = VCSMiddleware(
146 vcs_app = VCSMiddleware(
147 HTTPNotFound(), request.registry, settings,
147 HTTPNotFound(), request.registry, settings,
148 appenlight_client=ae_client)
148 appenlight_client=ae_client)
149
149
150 return wsgiapp(vcs_app)(None, request)
150 return wsgiapp(vcs_app)(None, request)
151
151
152
152
153 def error_handler(exception, request):
153 def error_handler(exception, request):
154 import rhodecode
154 import rhodecode
155 from rhodecode.lib import helpers
155 from rhodecode.lib import helpers
156
156
157 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
157 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
158
158
159 base_response = HTTPInternalServerError()
159 base_response = HTTPInternalServerError()
160 # prefer original exception for the response since it may have headers set
160 # prefer original exception for the response since it may have headers set
161 if isinstance(exception, HTTPException):
161 if isinstance(exception, HTTPException):
162 base_response = exception
162 base_response = exception
163 elif isinstance(exception, VCSCommunicationError):
163 elif isinstance(exception, VCSCommunicationError):
164 base_response = VCSServerUnavailable()
164 base_response = VCSServerUnavailable()
165
165
166 if is_http_error(base_response):
166 if is_http_error(base_response):
167 log.exception(
167 log.exception(
168 'error occurred handling this request for path: %s', request.path)
168 'error occurred handling this request for path: %s', request.path)
169
169
170 error_explanation = base_response.explanation or str(base_response)
170 error_explanation = base_response.explanation or str(base_response)
171 if base_response.status_code == 404:
171 if base_response.status_code == 404:
172 error_explanation += " Optionally you don't have permission to access this page."
172 error_explanation += " Optionally you don't have permission to access this page."
173 c = AttributeDict()
173 c = AttributeDict()
174 c.error_message = base_response.status
174 c.error_message = base_response.status
175 c.error_explanation = error_explanation
175 c.error_explanation = error_explanation
176 c.visual = AttributeDict()
176 c.visual = AttributeDict()
177
177
178 c.visual.rhodecode_support_url = (
178 c.visual.rhodecode_support_url = (
179 request.registry.settings.get('rhodecode_support_url') or
179 request.registry.settings.get('rhodecode_support_url') or
180 request.route_url('rhodecode_support')
180 request.route_url('rhodecode_support')
181 )
181 )
182 c.redirect_time = 0
182 c.redirect_time = 0
183 c.rhodecode_name = rhodecode_title
183 c.rhodecode_name = rhodecode_title
184 if not c.rhodecode_name:
184 if not c.rhodecode_name:
185 c.rhodecode_name = 'Rhodecode'
185 c.rhodecode_name = 'Rhodecode'
186
186
187 c.causes = []
187 c.causes = []
188 if is_http_error(base_response):
188 if is_http_error(base_response):
189 c.causes.append('Server is overloaded.')
189 c.causes.append('Server is overloaded.')
190 c.causes.append('Server database connection is lost.')
190 c.causes.append('Server database connection is lost.')
191 c.causes.append('Server expected unhandled error.')
191 c.causes.append('Server expected unhandled error.')
192
192
193 if hasattr(base_response, 'causes'):
193 if hasattr(base_response, 'causes'):
194 c.causes = base_response.causes
194 c.causes = base_response.causes
195
195
196 c.messages = helpers.flash.pop_messages(request=request)
196 c.messages = helpers.flash.pop_messages(request=request)
197
197
198 exc_info = sys.exc_info()
198 exc_info = sys.exc_info()
199 c.exception_id = id(exc_info)
199 c.exception_id = id(exc_info)
200 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
200 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
201 or base_response.status_code > 499
201 or base_response.status_code > 499
202 c.exception_id_url = request.route_url(
202 c.exception_id_url = request.route_url(
203 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
203 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
204
204
205 if c.show_exception_id:
205 if c.show_exception_id:
206 store_exception(c.exception_id, exc_info)
206 store_exception(c.exception_id, exc_info)
207
207
208 response = render_to_response(
208 response = render_to_response(
209 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
209 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
210 response=base_response)
210 response=base_response)
211
211
212 return response
212 return response
213
213
214
214
215 def includeme_first(config):
215 def includeme_first(config):
216 # redirect automatic browser favicon.ico requests to correct place
216 # redirect automatic browser favicon.ico requests to correct place
217 def favicon_redirect(context, request):
217 def favicon_redirect(context, request):
218 return HTTPFound(
218 return HTTPFound(
219 request.static_path('rhodecode:public/images/favicon.ico'))
219 request.static_path('rhodecode:public/images/favicon.ico'))
220
220
221 config.add_view(favicon_redirect, route_name='favicon')
221 config.add_view(favicon_redirect, route_name='favicon')
222 config.add_route('favicon', '/favicon.ico')
222 config.add_route('favicon', '/favicon.ico')
223
223
224 def robots_redirect(context, request):
224 def robots_redirect(context, request):
225 return HTTPFound(
225 return HTTPFound(
226 request.static_path('rhodecode:public/robots.txt'))
226 request.static_path('rhodecode:public/robots.txt'))
227
227
228 config.add_view(robots_redirect, route_name='robots')
228 config.add_view(robots_redirect, route_name='robots')
229 config.add_route('robots', '/robots.txt')
229 config.add_route('robots', '/robots.txt')
230
230
231 config.add_static_view(
231 config.add_static_view(
232 '_static/deform', 'deform:static')
232 '_static/deform', 'deform:static')
233 config.add_static_view(
233 config.add_static_view(
234 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
234 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
235
235
236
236
237 def includeme(config):
237 def includeme(config):
238 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
238 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
239 settings = config.registry.settings
239 settings = config.registry.settings
240 config.set_request_factory(Request)
240 config.set_request_factory(Request)
241
241
242 # plugin information
242 # plugin information
243 config.registry.rhodecode_plugins = collections.OrderedDict()
243 config.registry.rhodecode_plugins = collections.OrderedDict()
244
244
245 config.add_directive(
245 config.add_directive(
246 'register_rhodecode_plugin', register_rhodecode_plugin)
246 'register_rhodecode_plugin', register_rhodecode_plugin)
247
247
248 config.add_directive('configure_celery', configure_celery)
248 config.add_directive('configure_celery', configure_celery)
249
249
250 if asbool(settings.get('appenlight', 'false')):
250 if asbool(settings.get('appenlight', 'false')):
251 config.include('appenlight_client.ext.pyramid_tween')
251 config.include('appenlight_client.ext.pyramid_tween')
252
252
253 load_all = should_load_all()
253 load_all = should_load_all()
254
254
255 # Includes which are required. The application would fail without them.
255 # Includes which are required. The application would fail without them.
256 config.include('pyramid_mako')
256 config.include('pyramid_mako')
257 config.include('rhodecode.lib.rc_beaker')
257 config.include('rhodecode.lib.rc_beaker')
258 config.include('rhodecode.lib.rc_cache')
258 config.include('rhodecode.lib.rc_cache')
259
259
260 config.include('rhodecode.apps._base.navigation')
260 config.include('rhodecode.apps._base.navigation')
261 config.include('rhodecode.apps._base.subscribers')
261 config.include('rhodecode.apps._base.subscribers')
262 config.include('rhodecode.tweens')
262 config.include('rhodecode.tweens')
263 config.include('rhodecode.authentication')
263 config.include('rhodecode.authentication')
264
264
265 if load_all:
265 if load_all:
266 config.include('rhodecode.integrations')
266 config.include('rhodecode.integrations')
267
267
268 if load_all:
268 if load_all:
269 # load CE authentication plugins
269 # load CE authentication plugins
270 config.include('rhodecode.authentication.plugins.auth_crowd')
270 config.include('rhodecode.authentication.plugins.auth_crowd')
271 config.include('rhodecode.authentication.plugins.auth_headers')
271 config.include('rhodecode.authentication.plugins.auth_headers')
272 config.include('rhodecode.authentication.plugins.auth_jasig_cas')
272 config.include('rhodecode.authentication.plugins.auth_jasig_cas')
273 config.include('rhodecode.authentication.plugins.auth_ldap')
273 config.include('rhodecode.authentication.plugins.auth_ldap')
274 config.include('rhodecode.authentication.plugins.auth_pam')
274 config.include('rhodecode.authentication.plugins.auth_pam')
275 config.include('rhodecode.authentication.plugins.auth_rhodecode')
275 config.include('rhodecode.authentication.plugins.auth_rhodecode')
276 config.include('rhodecode.authentication.plugins.auth_token')
276 config.include('rhodecode.authentication.plugins.auth_token')
277
277
278 # Auto discover authentication plugins and include their configuration.
278 # Auto discover authentication plugins and include their configuration.
279 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
279 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
280 from rhodecode.authentication import discover_legacy_plugins
280 from rhodecode.authentication import discover_legacy_plugins
281 discover_legacy_plugins(config)
281 discover_legacy_plugins(config)
282
282
283 # apps
283 # apps
284 if load_all:
284 if load_all:
285 config.include('rhodecode.apps._base')
285 config.include('rhodecode.apps._base')
286 config.include('rhodecode.apps.hovercards')
286 config.include('rhodecode.apps.hovercards')
287 config.include('rhodecode.apps.ops')
287 config.include('rhodecode.apps.ops')
288 config.include('rhodecode.apps.admin')
288 config.include('rhodecode.apps.admin')
289 config.include('rhodecode.apps.channelstream')
289 config.include('rhodecode.apps.channelstream')
290 config.include('rhodecode.apps.file_store')
290 config.include('rhodecode.apps.file_store')
291 config.include('rhodecode.apps.login')
291 config.include('rhodecode.apps.login')
292 config.include('rhodecode.apps.home')
292 config.include('rhodecode.apps.home')
293 config.include('rhodecode.apps.journal')
293 config.include('rhodecode.apps.journal')
294 config.include('rhodecode.apps.repository')
294 config.include('rhodecode.apps.repository')
295 config.include('rhodecode.apps.repo_group')
295 config.include('rhodecode.apps.repo_group')
296 config.include('rhodecode.apps.user_group')
296 config.include('rhodecode.apps.user_group')
297 config.include('rhodecode.apps.search')
297 config.include('rhodecode.apps.search')
298 config.include('rhodecode.apps.user_profile')
298 config.include('rhodecode.apps.user_profile')
299 config.include('rhodecode.apps.user_group_profile')
299 config.include('rhodecode.apps.user_group_profile')
300 config.include('rhodecode.apps.my_account')
300 config.include('rhodecode.apps.my_account')
301 config.include('rhodecode.apps.svn_support')
301 config.include('rhodecode.apps.svn_support')
302 config.include('rhodecode.apps.ssh_support')
302 config.include('rhodecode.apps.ssh_support')
303 config.include('rhodecode.apps.gist')
303 config.include('rhodecode.apps.gist')
304 config.include('rhodecode.apps.debug_style')
304 config.include('rhodecode.apps.debug_style')
305 config.include('rhodecode.api')
305 config.include('rhodecode.api')
306
306
307 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
307 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
308 config.add_translation_dirs('rhodecode:i18n/')
308 config.add_translation_dirs('rhodecode:i18n/')
309 settings['default_locale_name'] = settings.get('lang', 'en')
309 settings['default_locale_name'] = settings.get('lang', 'en')
310
310
311 # Add subscribers.
311 # Add subscribers.
312 if load_all:
312 if load_all:
313 config.add_subscriber(inject_app_settings,
313 config.add_subscriber(inject_app_settings,
314 pyramid.events.ApplicationCreated)
314 pyramid.events.ApplicationCreated)
315 config.add_subscriber(scan_repositories_if_enabled,
315 config.add_subscriber(scan_repositories_if_enabled,
316 pyramid.events.ApplicationCreated)
316 pyramid.events.ApplicationCreated)
317 config.add_subscriber(write_metadata_if_needed,
317 config.add_subscriber(write_metadata_if_needed,
318 pyramid.events.ApplicationCreated)
318 pyramid.events.ApplicationCreated)
319 config.add_subscriber(write_usage_data,
319 config.add_subscriber(write_usage_data,
320 pyramid.events.ApplicationCreated)
320 pyramid.events.ApplicationCreated)
321 config.add_subscriber(write_js_routes_if_enabled,
321 config.add_subscriber(write_js_routes_if_enabled,
322 pyramid.events.ApplicationCreated)
322 pyramid.events.ApplicationCreated)
323
323
324 # request custom methods
324 # request custom methods
325 config.add_request_method(
325 config.add_request_method(
326 'rhodecode.lib.partial_renderer.get_partial_renderer',
326 'rhodecode.lib.partial_renderer.get_partial_renderer',
327 'get_partial_renderer')
327 'get_partial_renderer')
328
328
329 config.add_request_method(
329 config.add_request_method(
330 'rhodecode.lib.request_counter.get_request_counter',
330 'rhodecode.lib.request_counter.get_request_counter',
331 'request_count')
331 'request_count')
332
332
333 # Set the authorization policy.
333 # Set the authorization policy.
334 authz_policy = ACLAuthorizationPolicy()
334 authz_policy = ACLAuthorizationPolicy()
335 config.set_authorization_policy(authz_policy)
335 config.set_authorization_policy(authz_policy)
336
336
337 # Set the default renderer for HTML templates to mako.
337 # Set the default renderer for HTML templates to mako.
338 config.add_mako_renderer('.html')
338 config.add_mako_renderer('.html')
339
339
340 config.add_renderer(
340 config.add_renderer(
341 name='json_ext',
341 name='json_ext',
342 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
342 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
343
343
344 config.add_renderer(
345 name='string_html',
346 factory='rhodecode.lib.string_renderer.html')
347
344 # include RhodeCode plugins
348 # include RhodeCode plugins
345 includes = aslist(settings.get('rhodecode.includes', []))
349 includes = aslist(settings.get('rhodecode.includes', []))
346 for inc in includes:
350 for inc in includes:
347 config.include(inc)
351 config.include(inc)
348
352
349 # custom not found view, if our pyramid app doesn't know how to handle
353 # custom not found view, if our pyramid app doesn't know how to handle
350 # the request pass it to potential VCS handling ap
354 # the request pass it to potential VCS handling ap
351 config.add_notfound_view(not_found_view)
355 config.add_notfound_view(not_found_view)
352 if not settings.get('debugtoolbar.enabled', False):
356 if not settings.get('debugtoolbar.enabled', False):
353 # disabled debugtoolbar handle all exceptions via the error_handlers
357 # disabled debugtoolbar handle all exceptions via the error_handlers
354 config.add_view(error_handler, context=Exception)
358 config.add_view(error_handler, context=Exception)
355
359
356 # all errors including 403/404/50X
360 # all errors including 403/404/50X
357 config.add_view(error_handler, context=HTTPError)
361 config.add_view(error_handler, context=HTTPError)
358
362
359
363
360 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
364 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
361 """
365 """
362 Apply outer WSGI middlewares around the application.
366 Apply outer WSGI middlewares around the application.
363 """
367 """
364 registry = config.registry
368 registry = config.registry
365 settings = registry.settings
369 settings = registry.settings
366
370
367 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
371 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
368 pyramid_app = HttpsFixup(pyramid_app, settings)
372 pyramid_app = HttpsFixup(pyramid_app, settings)
369
373
370 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
374 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
371 pyramid_app, settings)
375 pyramid_app, settings)
372 registry.ae_client = _ae_client
376 registry.ae_client = _ae_client
373
377
374 if settings['gzip_responses']:
378 if settings['gzip_responses']:
375 pyramid_app = make_gzip_middleware(
379 pyramid_app = make_gzip_middleware(
376 pyramid_app, settings, compress_level=1)
380 pyramid_app, settings, compress_level=1)
377
381
378 # this should be the outer most middleware in the wsgi stack since
382 # this should be the outer most middleware in the wsgi stack since
379 # middleware like Routes make database calls
383 # middleware like Routes make database calls
380 def pyramid_app_with_cleanup(environ, start_response):
384 def pyramid_app_with_cleanup(environ, start_response):
381 try:
385 try:
382 return pyramid_app(environ, start_response)
386 return pyramid_app(environ, start_response)
383 finally:
387 finally:
384 # Dispose current database session and rollback uncommitted
388 # Dispose current database session and rollback uncommitted
385 # transactions.
389 # transactions.
386 meta.Session.remove()
390 meta.Session.remove()
387
391
388 # In a single threaded mode server, on non sqlite db we should have
392 # In a single threaded mode server, on non sqlite db we should have
389 # '0 Current Checked out connections' at the end of a request,
393 # '0 Current Checked out connections' at the end of a request,
390 # if not, then something, somewhere is leaving a connection open
394 # if not, then something, somewhere is leaving a connection open
391 pool = meta.Base.metadata.bind.engine.pool
395 pool = meta.Base.metadata.bind.engine.pool
392 log.debug('sa pool status: %s', pool.status())
396 log.debug('sa pool status: %s', pool.status())
393 log.debug('Request processing finalized')
397 log.debug('Request processing finalized')
394
398
395 return pyramid_app_with_cleanup
399 return pyramid_app_with_cleanup
396
400
397
401
398 def sanitize_settings_and_apply_defaults(global_config, settings):
402 def sanitize_settings_and_apply_defaults(global_config, settings):
399 """
403 """
400 Applies settings defaults and does all type conversion.
404 Applies settings defaults and does all type conversion.
401
405
402 We would move all settings parsing and preparation into this place, so that
406 We would move all settings parsing and preparation into this place, so that
403 we have only one place left which deals with this part. The remaining parts
407 we have only one place left which deals with this part. The remaining parts
404 of the application would start to rely fully on well prepared settings.
408 of the application would start to rely fully on well prepared settings.
405
409
406 This piece would later be split up per topic to avoid a big fat monster
410 This piece would later be split up per topic to avoid a big fat monster
407 function.
411 function.
408 """
412 """
409
413
410 settings.setdefault('rhodecode.edition', 'Community Edition')
414 settings.setdefault('rhodecode.edition', 'Community Edition')
411
415
412 if 'mako.default_filters' not in settings:
416 if 'mako.default_filters' not in settings:
413 # set custom default filters if we don't have it defined
417 # set custom default filters if we don't have it defined
414 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
418 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
415 settings['mako.default_filters'] = 'h_filter'
419 settings['mako.default_filters'] = 'h_filter'
416
420
417 if 'mako.directories' not in settings:
421 if 'mako.directories' not in settings:
418 mako_directories = settings.setdefault('mako.directories', [
422 mako_directories = settings.setdefault('mako.directories', [
419 # Base templates of the original application
423 # Base templates of the original application
420 'rhodecode:templates',
424 'rhodecode:templates',
421 ])
425 ])
422 log.debug(
426 log.debug(
423 "Using the following Mako template directories: %s",
427 "Using the following Mako template directories: %s",
424 mako_directories)
428 mako_directories)
425
429
426 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
430 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
427 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
431 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
428 raw_url = settings['beaker.session.url']
432 raw_url = settings['beaker.session.url']
429 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
433 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
430 settings['beaker.session.url'] = 'redis://' + raw_url
434 settings['beaker.session.url'] = 'redis://' + raw_url
431
435
432 # Default includes, possible to change as a user
436 # Default includes, possible to change as a user
433 pyramid_includes = settings.setdefault('pyramid.includes', [])
437 pyramid_includes = settings.setdefault('pyramid.includes', [])
434 log.debug(
438 log.debug(
435 "Using the following pyramid.includes: %s",
439 "Using the following pyramid.includes: %s",
436 pyramid_includes)
440 pyramid_includes)
437
441
438 # TODO: johbo: Re-think this, usually the call to config.include
442 # TODO: johbo: Re-think this, usually the call to config.include
439 # should allow to pass in a prefix.
443 # should allow to pass in a prefix.
440 settings.setdefault('rhodecode.api.url', '/_admin/api')
444 settings.setdefault('rhodecode.api.url', '/_admin/api')
441 settings.setdefault('__file__', global_config.get('__file__'))
445 settings.setdefault('__file__', global_config.get('__file__'))
442
446
443 # Sanitize generic settings.
447 # Sanitize generic settings.
444 _list_setting(settings, 'default_encoding', 'UTF-8')
448 _list_setting(settings, 'default_encoding', 'UTF-8')
445 _bool_setting(settings, 'is_test', 'false')
449 _bool_setting(settings, 'is_test', 'false')
446 _bool_setting(settings, 'gzip_responses', 'false')
450 _bool_setting(settings, 'gzip_responses', 'false')
447
451
448 # Call split out functions that sanitize settings for each topic.
452 # Call split out functions that sanitize settings for each topic.
449 _sanitize_appenlight_settings(settings)
453 _sanitize_appenlight_settings(settings)
450 _sanitize_vcs_settings(settings)
454 _sanitize_vcs_settings(settings)
451 _sanitize_cache_settings(settings)
455 _sanitize_cache_settings(settings)
452
456
453 # configure instance id
457 # configure instance id
454 config_utils.set_instance_id(settings)
458 config_utils.set_instance_id(settings)
455
459
456 return settings
460 return settings
457
461
458
462
459 def enable_debug():
463 def enable_debug():
460 """
464 """
461 Helper to enable debug on running instance
465 Helper to enable debug on running instance
462 :return:
466 :return:
463 """
467 """
464 import tempfile
468 import tempfile
465 import textwrap
469 import textwrap
466 import logging.config
470 import logging.config
467
471
468 ini_template = textwrap.dedent("""
472 ini_template = textwrap.dedent("""
469 #####################################
473 #####################################
470 ### DEBUG LOGGING CONFIGURATION ####
474 ### DEBUG LOGGING CONFIGURATION ####
471 #####################################
475 #####################################
472 [loggers]
476 [loggers]
473 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
477 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
474
478
475 [handlers]
479 [handlers]
476 keys = console, console_sql
480 keys = console, console_sql
477
481
478 [formatters]
482 [formatters]
479 keys = generic, color_formatter, color_formatter_sql
483 keys = generic, color_formatter, color_formatter_sql
480
484
481 #############
485 #############
482 ## LOGGERS ##
486 ## LOGGERS ##
483 #############
487 #############
484 [logger_root]
488 [logger_root]
485 level = NOTSET
489 level = NOTSET
486 handlers = console
490 handlers = console
487
491
488 [logger_sqlalchemy]
492 [logger_sqlalchemy]
489 level = INFO
493 level = INFO
490 handlers = console_sql
494 handlers = console_sql
491 qualname = sqlalchemy.engine
495 qualname = sqlalchemy.engine
492 propagate = 0
496 propagate = 0
493
497
494 [logger_beaker]
498 [logger_beaker]
495 level = DEBUG
499 level = DEBUG
496 handlers =
500 handlers =
497 qualname = beaker.container
501 qualname = beaker.container
498 propagate = 1
502 propagate = 1
499
503
500 [logger_rhodecode]
504 [logger_rhodecode]
501 level = DEBUG
505 level = DEBUG
502 handlers =
506 handlers =
503 qualname = rhodecode
507 qualname = rhodecode
504 propagate = 1
508 propagate = 1
505
509
506 [logger_ssh_wrapper]
510 [logger_ssh_wrapper]
507 level = DEBUG
511 level = DEBUG
508 handlers =
512 handlers =
509 qualname = ssh_wrapper
513 qualname = ssh_wrapper
510 propagate = 1
514 propagate = 1
511
515
512 [logger_celery]
516 [logger_celery]
513 level = DEBUG
517 level = DEBUG
514 handlers =
518 handlers =
515 qualname = celery
519 qualname = celery
516
520
517
521
518 ##############
522 ##############
519 ## HANDLERS ##
523 ## HANDLERS ##
520 ##############
524 ##############
521
525
522 [handler_console]
526 [handler_console]
523 class = StreamHandler
527 class = StreamHandler
524 args = (sys.stderr, )
528 args = (sys.stderr, )
525 level = DEBUG
529 level = DEBUG
526 formatter = color_formatter
530 formatter = color_formatter
527
531
528 [handler_console_sql]
532 [handler_console_sql]
529 # "level = DEBUG" logs SQL queries and results.
533 # "level = DEBUG" logs SQL queries and results.
530 # "level = INFO" logs SQL queries.
534 # "level = INFO" logs SQL queries.
531 # "level = WARN" logs neither. (Recommended for production systems.)
535 # "level = WARN" logs neither. (Recommended for production systems.)
532 class = StreamHandler
536 class = StreamHandler
533 args = (sys.stderr, )
537 args = (sys.stderr, )
534 level = WARN
538 level = WARN
535 formatter = color_formatter_sql
539 formatter = color_formatter_sql
536
540
537 ################
541 ################
538 ## FORMATTERS ##
542 ## FORMATTERS ##
539 ################
543 ################
540
544
541 [formatter_generic]
545 [formatter_generic]
542 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
546 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
543 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
547 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
544 datefmt = %Y-%m-%d %H:%M:%S
548 datefmt = %Y-%m-%d %H:%M:%S
545
549
546 [formatter_color_formatter]
550 [formatter_color_formatter]
547 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
551 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
548 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
552 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
549 datefmt = %Y-%m-%d %H:%M:%S
553 datefmt = %Y-%m-%d %H:%M:%S
550
554
551 [formatter_color_formatter_sql]
555 [formatter_color_formatter_sql]
552 class = rhodecode.lib.logging_formatter.ColorFormatterSql
556 class = rhodecode.lib.logging_formatter.ColorFormatterSql
553 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
557 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
554 datefmt = %Y-%m-%d %H:%M:%S
558 datefmt = %Y-%m-%d %H:%M:%S
555 """)
559 """)
556
560
557 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
561 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
558 delete=False) as f:
562 delete=False) as f:
559 log.info('Saved Temporary DEBUG config at %s', f.name)
563 log.info('Saved Temporary DEBUG config at %s', f.name)
560 f.write(ini_template)
564 f.write(ini_template)
561
565
562 logging.config.fileConfig(f.name)
566 logging.config.fileConfig(f.name)
563 log.debug('DEBUG MODE ON')
567 log.debug('DEBUG MODE ON')
564 os.remove(f.name)
568 os.remove(f.name)
565
569
566
570
567 def _sanitize_appenlight_settings(settings):
571 def _sanitize_appenlight_settings(settings):
568 _bool_setting(settings, 'appenlight', 'false')
572 _bool_setting(settings, 'appenlight', 'false')
569
573
570
574
571 def _sanitize_vcs_settings(settings):
575 def _sanitize_vcs_settings(settings):
572 """
576 """
573 Applies settings defaults and does type conversion for all VCS related
577 Applies settings defaults and does type conversion for all VCS related
574 settings.
578 settings.
575 """
579 """
576 _string_setting(settings, 'vcs.svn.compatible_version', '')
580 _string_setting(settings, 'vcs.svn.compatible_version', '')
577 _string_setting(settings, 'vcs.hooks.protocol', 'http')
581 _string_setting(settings, 'vcs.hooks.protocol', 'http')
578 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
582 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
579 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
583 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
580 _string_setting(settings, 'vcs.server', '')
584 _string_setting(settings, 'vcs.server', '')
581 _string_setting(settings, 'vcs.server.protocol', 'http')
585 _string_setting(settings, 'vcs.server.protocol', 'http')
582 _bool_setting(settings, 'startup.import_repos', 'false')
586 _bool_setting(settings, 'startup.import_repos', 'false')
583 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
587 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
584 _bool_setting(settings, 'vcs.server.enable', 'true')
588 _bool_setting(settings, 'vcs.server.enable', 'true')
585 _bool_setting(settings, 'vcs.start_server', 'false')
589 _bool_setting(settings, 'vcs.start_server', 'false')
586 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
590 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
587 _int_setting(settings, 'vcs.connection_timeout', 3600)
591 _int_setting(settings, 'vcs.connection_timeout', 3600)
588
592
589 # Support legacy values of vcs.scm_app_implementation. Legacy
593 # Support legacy values of vcs.scm_app_implementation. Legacy
590 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
594 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
591 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
595 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
592 scm_app_impl = settings['vcs.scm_app_implementation']
596 scm_app_impl = settings['vcs.scm_app_implementation']
593 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
597 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
594 settings['vcs.scm_app_implementation'] = 'http'
598 settings['vcs.scm_app_implementation'] = 'http'
595
599
596
600
597 def _sanitize_cache_settings(settings):
601 def _sanitize_cache_settings(settings):
598 temp_store = tempfile.gettempdir()
602 temp_store = tempfile.gettempdir()
599 default_cache_dir = os.path.join(temp_store, 'rc_cache')
603 default_cache_dir = os.path.join(temp_store, 'rc_cache')
600
604
601 # save default, cache dir, and use it for all backends later.
605 # save default, cache dir, and use it for all backends later.
602 default_cache_dir = _string_setting(
606 default_cache_dir = _string_setting(
603 settings,
607 settings,
604 'cache_dir',
608 'cache_dir',
605 default_cache_dir, lower=False, default_when_empty=True)
609 default_cache_dir, lower=False, default_when_empty=True)
606
610
607 # ensure we have our dir created
611 # ensure we have our dir created
608 if not os.path.isdir(default_cache_dir):
612 if not os.path.isdir(default_cache_dir):
609 os.makedirs(default_cache_dir, mode=0o755)
613 os.makedirs(default_cache_dir, mode=0o755)
610
614
611 # exception store cache
615 # exception store cache
612 _string_setting(
616 _string_setting(
613 settings,
617 settings,
614 'exception_tracker.store_path',
618 'exception_tracker.store_path',
615 temp_store, lower=False, default_when_empty=True)
619 temp_store, lower=False, default_when_empty=True)
616 _bool_setting(
620 _bool_setting(
617 settings,
621 settings,
618 'exception_tracker.send_email',
622 'exception_tracker.send_email',
619 'false')
623 'false')
620 _string_setting(
624 _string_setting(
621 settings,
625 settings,
622 'exception_tracker.email_prefix',
626 'exception_tracker.email_prefix',
623 '[RHODECODE ERROR]', lower=False, default_when_empty=True)
627 '[RHODECODE ERROR]', lower=False, default_when_empty=True)
624
628
625 # cache_perms
629 # cache_perms
626 _string_setting(
630 _string_setting(
627 settings,
631 settings,
628 'rc_cache.cache_perms.backend',
632 'rc_cache.cache_perms.backend',
629 'dogpile.cache.rc.file_namespace', lower=False)
633 'dogpile.cache.rc.file_namespace', lower=False)
630 _int_setting(
634 _int_setting(
631 settings,
635 settings,
632 'rc_cache.cache_perms.expiration_time',
636 'rc_cache.cache_perms.expiration_time',
633 60)
637 60)
634 _string_setting(
638 _string_setting(
635 settings,
639 settings,
636 'rc_cache.cache_perms.arguments.filename',
640 'rc_cache.cache_perms.arguments.filename',
637 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
641 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
638
642
639 # cache_repo
643 # cache_repo
640 _string_setting(
644 _string_setting(
641 settings,
645 settings,
642 'rc_cache.cache_repo.backend',
646 'rc_cache.cache_repo.backend',
643 'dogpile.cache.rc.file_namespace', lower=False)
647 'dogpile.cache.rc.file_namespace', lower=False)
644 _int_setting(
648 _int_setting(
645 settings,
649 settings,
646 'rc_cache.cache_repo.expiration_time',
650 'rc_cache.cache_repo.expiration_time',
647 60)
651 60)
648 _string_setting(
652 _string_setting(
649 settings,
653 settings,
650 'rc_cache.cache_repo.arguments.filename',
654 'rc_cache.cache_repo.arguments.filename',
651 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
655 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
652
656
653 # cache_license
657 # cache_license
654 _string_setting(
658 _string_setting(
655 settings,
659 settings,
656 'rc_cache.cache_license.backend',
660 'rc_cache.cache_license.backend',
657 'dogpile.cache.rc.file_namespace', lower=False)
661 'dogpile.cache.rc.file_namespace', lower=False)
658 _int_setting(
662 _int_setting(
659 settings,
663 settings,
660 'rc_cache.cache_license.expiration_time',
664 'rc_cache.cache_license.expiration_time',
661 5*60)
665 5*60)
662 _string_setting(
666 _string_setting(
663 settings,
667 settings,
664 'rc_cache.cache_license.arguments.filename',
668 'rc_cache.cache_license.arguments.filename',
665 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
669 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
666
670
667 # cache_repo_longterm memory, 96H
671 # cache_repo_longterm memory, 96H
668 _string_setting(
672 _string_setting(
669 settings,
673 settings,
670 'rc_cache.cache_repo_longterm.backend',
674 'rc_cache.cache_repo_longterm.backend',
671 'dogpile.cache.rc.memory_lru', lower=False)
675 'dogpile.cache.rc.memory_lru', lower=False)
672 _int_setting(
676 _int_setting(
673 settings,
677 settings,
674 'rc_cache.cache_repo_longterm.expiration_time',
678 'rc_cache.cache_repo_longterm.expiration_time',
675 345600)
679 345600)
676 _int_setting(
680 _int_setting(
677 settings,
681 settings,
678 'rc_cache.cache_repo_longterm.max_size',
682 'rc_cache.cache_repo_longterm.max_size',
679 10000)
683 10000)
680
684
681 # sql_cache_short
685 # sql_cache_short
682 _string_setting(
686 _string_setting(
683 settings,
687 settings,
684 'rc_cache.sql_cache_short.backend',
688 'rc_cache.sql_cache_short.backend',
685 'dogpile.cache.rc.memory_lru', lower=False)
689 'dogpile.cache.rc.memory_lru', lower=False)
686 _int_setting(
690 _int_setting(
687 settings,
691 settings,
688 'rc_cache.sql_cache_short.expiration_time',
692 'rc_cache.sql_cache_short.expiration_time',
689 30)
693 30)
690 _int_setting(
694 _int_setting(
691 settings,
695 settings,
692 'rc_cache.sql_cache_short.max_size',
696 'rc_cache.sql_cache_short.max_size',
693 10000)
697 10000)
694
698
695
699
696 def _int_setting(settings, name, default):
700 def _int_setting(settings, name, default):
697 settings[name] = int(settings.get(name, default))
701 settings[name] = int(settings.get(name, default))
698 return settings[name]
702 return settings[name]
699
703
700
704
701 def _bool_setting(settings, name, default):
705 def _bool_setting(settings, name, default):
702 input_val = settings.get(name, default)
706 input_val = settings.get(name, default)
703 if isinstance(input_val, unicode):
707 if isinstance(input_val, unicode):
704 input_val = input_val.encode('utf8')
708 input_val = input_val.encode('utf8')
705 settings[name] = asbool(input_val)
709 settings[name] = asbool(input_val)
706 return settings[name]
710 return settings[name]
707
711
708
712
709 def _list_setting(settings, name, default):
713 def _list_setting(settings, name, default):
710 raw_value = settings.get(name, default)
714 raw_value = settings.get(name, default)
711
715
712 old_separator = ','
716 old_separator = ','
713 if old_separator in raw_value:
717 if old_separator in raw_value:
714 # If we get a comma separated list, pass it to our own function.
718 # If we get a comma separated list, pass it to our own function.
715 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
719 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
716 else:
720 else:
717 # Otherwise we assume it uses pyramids space/newline separation.
721 # Otherwise we assume it uses pyramids space/newline separation.
718 settings[name] = aslist(raw_value)
722 settings[name] = aslist(raw_value)
719 return settings[name]
723 return settings[name]
720
724
721
725
722 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
726 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
723 value = settings.get(name, default)
727 value = settings.get(name, default)
724
728
725 if default_when_empty and not value:
729 if default_when_empty and not value:
726 # use default value when value is empty
730 # use default value when value is empty
727 value = default
731 value = default
728
732
729 if lower:
733 if lower:
730 value = value.lower()
734 value = value.lower()
731 settings[name] = value
735 settings[name] = value
732 return settings[name]
736 return settings[name]
733
737
734
738
735 def _substitute_values(mapping, substitutions):
739 def _substitute_values(mapping, substitutions):
736 result = {}
740 result = {}
737
741
738 try:
742 try:
739 for key, value in mapping.items():
743 for key, value in mapping.items():
740 # initialize without substitution first
744 # initialize without substitution first
741 result[key] = value
745 result[key] = value
742
746
743 # Note: Cannot use regular replacements, since they would clash
747 # Note: Cannot use regular replacements, since they would clash
744 # with the implementation of ConfigParser. Using "format" instead.
748 # with the implementation of ConfigParser. Using "format" instead.
745 try:
749 try:
746 result[key] = value.format(**substitutions)
750 result[key] = value.format(**substitutions)
747 except KeyError as e:
751 except KeyError as e:
748 env_var = '{}'.format(e.args[0])
752 env_var = '{}'.format(e.args[0])
749
753
750 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
754 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
751 'Make sure your environment has {var} set, or remove this ' \
755 'Make sure your environment has {var} set, or remove this ' \
752 'variable from config file'.format(key=key, var=env_var)
756 'variable from config file'.format(key=key, var=env_var)
753
757
754 if env_var.startswith('ENV_'):
758 if env_var.startswith('ENV_'):
755 raise ValueError(msg)
759 raise ValueError(msg)
756 else:
760 else:
757 log.warning(msg)
761 log.warning(msg)
758
762
759 except ValueError as e:
763 except ValueError as e:
760 log.warning('Failed to substitute ENV variable: %s', e)
764 log.warning('Failed to substitute ENV variable: %s', e)
761 result = mapping
765 result = mapping
762
766
763 return result
767 return result
@@ -1,295 +1,298 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2020 RhodeCode GmbH
3 # Copyright (C) 2017-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 import logging
21 import logging
22 import datetime
22 import datetime
23
23
24 from rhodecode.lib.jsonalchemy import JsonRaw
24 from rhodecode.lib.jsonalchemy import JsonRaw
25 from rhodecode.model import meta
25 from rhodecode.model import meta
26 from rhodecode.model.db import User, UserLog, Repository
26 from rhodecode.model.db import User, UserLog, Repository
27
27
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31 # action as key, and expected action_data as value
31 # action as key, and expected action_data as value
32 ACTIONS_V1 = {
32 ACTIONS_V1 = {
33 'user.login.success': {'user_agent': ''},
33 'user.login.success': {'user_agent': ''},
34 'user.login.failure': {'user_agent': ''},
34 'user.login.failure': {'user_agent': ''},
35 'user.logout': {'user_agent': ''},
35 'user.logout': {'user_agent': ''},
36 'user.register': {},
36 'user.register': {},
37 'user.password.reset_request': {},
37 'user.password.reset_request': {},
38 'user.push': {'user_agent': '', 'commit_ids': []},
38 'user.push': {'user_agent': '', 'commit_ids': []},
39 'user.pull': {'user_agent': ''},
39 'user.pull': {'user_agent': ''},
40
40
41 'user.create': {'data': {}},
41 'user.create': {'data': {}},
42 'user.delete': {'old_data': {}},
42 'user.delete': {'old_data': {}},
43 'user.edit': {'old_data': {}},
43 'user.edit': {'old_data': {}},
44 'user.edit.permissions': {},
44 'user.edit.permissions': {},
45 'user.edit.ip.add': {'ip': {}, 'user': {}},
45 'user.edit.ip.add': {'ip': {}, 'user': {}},
46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
47 'user.edit.token.add': {'token': {}, 'user': {}},
47 'user.edit.token.add': {'token': {}, 'user': {}},
48 'user.edit.token.delete': {'token': {}, 'user': {}},
48 'user.edit.token.delete': {'token': {}, 'user': {}},
49 'user.edit.email.add': {'email': ''},
49 'user.edit.email.add': {'email': ''},
50 'user.edit.email.delete': {'email': ''},
50 'user.edit.email.delete': {'email': ''},
51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
53 'user.edit.password_reset.enabled': {},
53 'user.edit.password_reset.enabled': {},
54 'user.edit.password_reset.disabled': {},
54 'user.edit.password_reset.disabled': {},
55
55
56 'user_group.create': {'data': {}},
56 'user_group.create': {'data': {}},
57 'user_group.delete': {'old_data': {}},
57 'user_group.delete': {'old_data': {}},
58 'user_group.edit': {'old_data': {}},
58 'user_group.edit': {'old_data': {}},
59 'user_group.edit.permissions': {},
59 'user_group.edit.permissions': {},
60 'user_group.edit.member.add': {'user': {}},
60 'user_group.edit.member.add': {'user': {}},
61 'user_group.edit.member.delete': {'user': {}},
61 'user_group.edit.member.delete': {'user': {}},
62
62
63 'repo.create': {'data': {}},
63 'repo.create': {'data': {}},
64 'repo.fork': {'data': {}},
64 'repo.fork': {'data': {}},
65 'repo.edit': {'old_data': {}},
65 'repo.edit': {'old_data': {}},
66 'repo.edit.permissions': {},
66 'repo.edit.permissions': {},
67 'repo.edit.permissions.branch': {},
67 'repo.edit.permissions.branch': {},
68 'repo.archive': {'old_data': {}},
68 'repo.archive': {'old_data': {}},
69 'repo.delete': {'old_data': {}},
69 'repo.delete': {'old_data': {}},
70
70
71 'repo.archive.download': {'user_agent': '', 'archive_name': '',
71 'repo.archive.download': {'user_agent': '', 'archive_name': '',
72 'archive_spec': '', 'archive_cached': ''},
72 'archive_spec': '', 'archive_cached': ''},
73
73
74 'repo.permissions.branch_rule.create': {},
74 'repo.permissions.branch_rule.create': {},
75 'repo.permissions.branch_rule.edit': {},
75 'repo.permissions.branch_rule.edit': {},
76 'repo.permissions.branch_rule.delete': {},
76 'repo.permissions.branch_rule.delete': {},
77
77
78 'repo.pull_request.create': '',
78 'repo.pull_request.create': '',
79 'repo.pull_request.edit': '',
79 'repo.pull_request.edit': '',
80 'repo.pull_request.delete': '',
80 'repo.pull_request.delete': '',
81 'repo.pull_request.close': '',
81 'repo.pull_request.close': '',
82 'repo.pull_request.merge': '',
82 'repo.pull_request.merge': '',
83 'repo.pull_request.vote': '',
83 'repo.pull_request.vote': '',
84 'repo.pull_request.comment.create': '',
84 'repo.pull_request.comment.create': '',
85 'repo.pull_request.comment.edit': '',
85 'repo.pull_request.comment.edit': '',
86 'repo.pull_request.comment.delete': '',
86 'repo.pull_request.comment.delete': '',
87
87
88 'repo.pull_request.reviewer.add': '',
88 'repo.pull_request.reviewer.add': '',
89 'repo.pull_request.reviewer.delete': '',
89 'repo.pull_request.reviewer.delete': '',
90
90
91 'repo.pull_request.observer.add': '',
92 'repo.pull_request.observer.delete': '',
93
91 'repo.commit.strip': {'commit_id': ''},
94 'repo.commit.strip': {'commit_id': ''},
92 'repo.commit.comment.create': {'data': {}},
95 'repo.commit.comment.create': {'data': {}},
93 'repo.commit.comment.delete': {'data': {}},
96 'repo.commit.comment.delete': {'data': {}},
94 'repo.commit.comment.edit': {'data': {}},
97 'repo.commit.comment.edit': {'data': {}},
95 'repo.commit.vote': '',
98 'repo.commit.vote': '',
96
99
97 'repo.artifact.add': '',
100 'repo.artifact.add': '',
98 'repo.artifact.delete': '',
101 'repo.artifact.delete': '',
99
102
100 'repo_group.create': {'data': {}},
103 'repo_group.create': {'data': {}},
101 'repo_group.edit': {'old_data': {}},
104 'repo_group.edit': {'old_data': {}},
102 'repo_group.edit.permissions': {},
105 'repo_group.edit.permissions': {},
103 'repo_group.delete': {'old_data': {}},
106 'repo_group.delete': {'old_data': {}},
104 }
107 }
105
108
106 ACTIONS = ACTIONS_V1
109 ACTIONS = ACTIONS_V1
107
110
108 SOURCE_WEB = 'source_web'
111 SOURCE_WEB = 'source_web'
109 SOURCE_API = 'source_api'
112 SOURCE_API = 'source_api'
110
113
111
114
112 class UserWrap(object):
115 class UserWrap(object):
113 """
116 """
114 Fake object used to imitate AuthUser
117 Fake object used to imitate AuthUser
115 """
118 """
116
119
117 def __init__(self, user_id=None, username=None, ip_addr=None):
120 def __init__(self, user_id=None, username=None, ip_addr=None):
118 self.user_id = user_id
121 self.user_id = user_id
119 self.username = username
122 self.username = username
120 self.ip_addr = ip_addr
123 self.ip_addr = ip_addr
121
124
122
125
123 class RepoWrap(object):
126 class RepoWrap(object):
124 """
127 """
125 Fake object used to imitate RepoObject that audit logger requires
128 Fake object used to imitate RepoObject that audit logger requires
126 """
129 """
127
130
128 def __init__(self, repo_id=None, repo_name=None):
131 def __init__(self, repo_id=None, repo_name=None):
129 self.repo_id = repo_id
132 self.repo_id = repo_id
130 self.repo_name = repo_name
133 self.repo_name = repo_name
131
134
132
135
133 def _store_log(action_name, action_data, user_id, username, user_data,
136 def _store_log(action_name, action_data, user_id, username, user_data,
134 ip_address, repository_id, repository_name):
137 ip_address, repository_id, repository_name):
135 user_log = UserLog()
138 user_log = UserLog()
136 user_log.version = UserLog.VERSION_2
139 user_log.version = UserLog.VERSION_2
137
140
138 user_log.action = action_name
141 user_log.action = action_name
139 user_log.action_data = action_data or JsonRaw(u'{}')
142 user_log.action_data = action_data or JsonRaw(u'{}')
140
143
141 user_log.user_ip = ip_address
144 user_log.user_ip = ip_address
142
145
143 user_log.user_id = user_id
146 user_log.user_id = user_id
144 user_log.username = username
147 user_log.username = username
145 user_log.user_data = user_data or JsonRaw(u'{}')
148 user_log.user_data = user_data or JsonRaw(u'{}')
146
149
147 user_log.repository_id = repository_id
150 user_log.repository_id = repository_id
148 user_log.repository_name = repository_name
151 user_log.repository_name = repository_name
149
152
150 user_log.action_date = datetime.datetime.now()
153 user_log.action_date = datetime.datetime.now()
151
154
152 return user_log
155 return user_log
153
156
154
157
155 def store_web(*args, **kwargs):
158 def store_web(*args, **kwargs):
156 action_data = {}
159 action_data = {}
157 org_action_data = kwargs.pop('action_data', {})
160 org_action_data = kwargs.pop('action_data', {})
158 action_data.update(org_action_data)
161 action_data.update(org_action_data)
159 action_data['source'] = SOURCE_WEB
162 action_data['source'] = SOURCE_WEB
160 kwargs['action_data'] = action_data
163 kwargs['action_data'] = action_data
161
164
162 return store(*args, **kwargs)
165 return store(*args, **kwargs)
163
166
164
167
165 def store_api(*args, **kwargs):
168 def store_api(*args, **kwargs):
166 action_data = {}
169 action_data = {}
167 org_action_data = kwargs.pop('action_data', {})
170 org_action_data = kwargs.pop('action_data', {})
168 action_data.update(org_action_data)
171 action_data.update(org_action_data)
169 action_data['source'] = SOURCE_API
172 action_data['source'] = SOURCE_API
170 kwargs['action_data'] = action_data
173 kwargs['action_data'] = action_data
171
174
172 return store(*args, **kwargs)
175 return store(*args, **kwargs)
173
176
174
177
175 def store(action, user, action_data=None, user_data=None, ip_addr=None,
178 def store(action, user, action_data=None, user_data=None, ip_addr=None,
176 repo=None, sa_session=None, commit=False):
179 repo=None, sa_session=None, commit=False):
177 """
180 """
178 Audit logger for various actions made by users, typically this
181 Audit logger for various actions made by users, typically this
179 results in a call such::
182 results in a call such::
180
183
181 from rhodecode.lib import audit_logger
184 from rhodecode.lib import audit_logger
182
185
183 audit_logger.store(
186 audit_logger.store(
184 'repo.edit', user=self._rhodecode_user)
187 'repo.edit', user=self._rhodecode_user)
185 audit_logger.store(
188 audit_logger.store(
186 'repo.delete', action_data={'data': repo_data},
189 'repo.delete', action_data={'data': repo_data},
187 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
190 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
188
191
189 # repo action
192 # repo action
190 audit_logger.store(
193 audit_logger.store(
191 'repo.delete',
194 'repo.delete',
192 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
195 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
193 repo=audit_logger.RepoWrap(repo_name='some-repo'))
196 repo=audit_logger.RepoWrap(repo_name='some-repo'))
194
197
195 # repo action, when we know and have the repository object already
198 # repo action, when we know and have the repository object already
196 audit_logger.store(
199 audit_logger.store(
197 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
200 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
198 user=self._rhodecode_user,
201 user=self._rhodecode_user,
199 repo=repo_object)
202 repo=repo_object)
200
203
201 # alternative wrapper to the above
204 # alternative wrapper to the above
202 audit_logger.store_web(
205 audit_logger.store_web(
203 'repo.delete', action_data={},
206 'repo.delete', action_data={},
204 user=self._rhodecode_user,
207 user=self._rhodecode_user,
205 repo=repo_object)
208 repo=repo_object)
206
209
207 # without an user ?
210 # without an user ?
208 audit_logger.store(
211 audit_logger.store(
209 'user.login.failure',
212 'user.login.failure',
210 user=audit_logger.UserWrap(
213 user=audit_logger.UserWrap(
211 username=self.request.params.get('username'),
214 username=self.request.params.get('username'),
212 ip_addr=self.request.remote_addr))
215 ip_addr=self.request.remote_addr))
213
216
214 """
217 """
215 from rhodecode.lib.utils2 import safe_unicode
218 from rhodecode.lib.utils2 import safe_unicode
216 from rhodecode.lib.auth import AuthUser
219 from rhodecode.lib.auth import AuthUser
217
220
218 action_spec = ACTIONS.get(action, None)
221 action_spec = ACTIONS.get(action, None)
219 if action_spec is None:
222 if action_spec is None:
220 raise ValueError('Action `{}` is not supported'.format(action))
223 raise ValueError('Action `{}` is not supported'.format(action))
221
224
222 if not sa_session:
225 if not sa_session:
223 sa_session = meta.Session()
226 sa_session = meta.Session()
224
227
225 try:
228 try:
226 username = getattr(user, 'username', None)
229 username = getattr(user, 'username', None)
227 if not username:
230 if not username:
228 pass
231 pass
229
232
230 user_id = getattr(user, 'user_id', None)
233 user_id = getattr(user, 'user_id', None)
231 if not user_id:
234 if not user_id:
232 # maybe we have username ? Try to figure user_id from username
235 # maybe we have username ? Try to figure user_id from username
233 if username:
236 if username:
234 user_id = getattr(
237 user_id = getattr(
235 User.get_by_username(username), 'user_id', None)
238 User.get_by_username(username), 'user_id', None)
236
239
237 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
240 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
238 if not ip_addr:
241 if not ip_addr:
239 pass
242 pass
240
243
241 if not user_data:
244 if not user_data:
242 # try to get this from the auth user
245 # try to get this from the auth user
243 if isinstance(user, AuthUser):
246 if isinstance(user, AuthUser):
244 user_data = {
247 user_data = {
245 'username': user.username,
248 'username': user.username,
246 'email': user.email,
249 'email': user.email,
247 }
250 }
248
251
249 repository_name = getattr(repo, 'repo_name', None)
252 repository_name = getattr(repo, 'repo_name', None)
250 repository_id = getattr(repo, 'repo_id', None)
253 repository_id = getattr(repo, 'repo_id', None)
251 if not repository_id:
254 if not repository_id:
252 # maybe we have repo_name ? Try to figure repo_id from repo_name
255 # maybe we have repo_name ? Try to figure repo_id from repo_name
253 if repository_name:
256 if repository_name:
254 repository_id = getattr(
257 repository_id = getattr(
255 Repository.get_by_repo_name(repository_name), 'repo_id', None)
258 Repository.get_by_repo_name(repository_name), 'repo_id', None)
256
259
257 action_name = safe_unicode(action)
260 action_name = safe_unicode(action)
258 ip_address = safe_unicode(ip_addr)
261 ip_address = safe_unicode(ip_addr)
259
262
260 with sa_session.no_autoflush:
263 with sa_session.no_autoflush:
261 update_user_last_activity(sa_session, user_id)
264 update_user_last_activity(sa_session, user_id)
262
265
263 user_log = _store_log(
266 user_log = _store_log(
264 action_name=action_name,
267 action_name=action_name,
265 action_data=action_data or {},
268 action_data=action_data or {},
266 user_id=user_id,
269 user_id=user_id,
267 username=username,
270 username=username,
268 user_data=user_data or {},
271 user_data=user_data or {},
269 ip_address=ip_address,
272 ip_address=ip_address,
270 repository_id=repository_id,
273 repository_id=repository_id,
271 repository_name=repository_name
274 repository_name=repository_name
272 )
275 )
273
276
274 sa_session.add(user_log)
277 sa_session.add(user_log)
275
278
276 if commit:
279 if commit:
277 sa_session.commit()
280 sa_session.commit()
278
281
279 entry_id = user_log.entry_id or ''
282 entry_id = user_log.entry_id or ''
280 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
283 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
281 entry_id, action_name, user_id, username, ip_address)
284 entry_id, action_name, user_id, username, ip_address)
282
285
283 except Exception:
286 except Exception:
284 log.exception('AUDIT: failed to store audit log')
287 log.exception('AUDIT: failed to store audit log')
285
288
286
289
287 def update_user_last_activity(sa_session, user_id):
290 def update_user_last_activity(sa_session, user_id):
288 _last_activity = datetime.datetime.now()
291 _last_activity = datetime.datetime.now()
289 try:
292 try:
290 sa_session.query(User).filter(User.user_id == user_id).update(
293 sa_session.query(User).filter(User.user_id == user_id).update(
291 {"last_activity": _last_activity})
294 {"last_activity": _last_activity})
292 log.debug(
295 log.debug(
293 'updated user `%s` last activity to:%s', user_id, _last_activity)
296 'updated user `%s` last activity to:%s', user_id, _last_activity)
294 except Exception:
297 except Exception:
295 log.exception("Failed last activity update")
298 log.exception("Failed last activity update")
@@ -1,2113 +1,2117 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27 import base64
27 import base64
28
28
29 import os
29 import os
30 import random
30 import random
31 import hashlib
31 import hashlib
32 import StringIO
32 import StringIO
33 import textwrap
33 import textwrap
34 import urllib
34 import urllib
35 import math
35 import math
36 import logging
36 import logging
37 import re
37 import re
38 import time
38 import time
39 import string
39 import string
40 import hashlib
40 import hashlib
41 import regex
41 import regex
42 from collections import OrderedDict
42 from collections import OrderedDict
43
43
44 import pygments
44 import pygments
45 import itertools
45 import itertools
46 import fnmatch
46 import fnmatch
47 import bleach
47 import bleach
48
48
49 from pyramid import compat
49 from pyramid import compat
50 from datetime import datetime
50 from datetime import datetime
51 from functools import partial
51 from functools import partial
52 from pygments.formatters.html import HtmlFormatter
52 from pygments.formatters.html import HtmlFormatter
53 from pygments.lexers import (
53 from pygments.lexers import (
54 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
54 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
55
55
56 from pyramid.threadlocal import get_current_request
56 from pyramid.threadlocal import get_current_request
57 from tempita import looper
57 from tempita import looper
58 from webhelpers2.html import literal, HTML, escape
58 from webhelpers2.html import literal, HTML, escape
59 from webhelpers2.html._autolink import _auto_link_urls
59 from webhelpers2.html._autolink import _auto_link_urls
60 from webhelpers2.html.tools import (
60 from webhelpers2.html.tools import (
61 button_to, highlight, js_obfuscate, strip_links, strip_tags)
61 button_to, highlight, js_obfuscate, strip_links, strip_tags)
62
62
63 from webhelpers2.text import (
63 from webhelpers2.text import (
64 chop_at, collapse, convert_accented_entities,
64 chop_at, collapse, convert_accented_entities,
65 convert_misc_entities, lchop, plural, rchop, remove_formatting,
65 convert_misc_entities, lchop, plural, rchop, remove_formatting,
66 replace_whitespace, urlify, truncate, wrap_paragraphs)
66 replace_whitespace, urlify, truncate, wrap_paragraphs)
67 from webhelpers2.date import time_ago_in_words
67 from webhelpers2.date import time_ago_in_words
68
68
69 from webhelpers2.html.tags import (
69 from webhelpers2.html.tags import (
70 _input, NotGiven, _make_safe_id_component as safeid,
70 _input, NotGiven, _make_safe_id_component as safeid,
71 form as insecure_form,
71 form as insecure_form,
72 auto_discovery_link, checkbox, end_form, file,
72 auto_discovery_link, checkbox, end_form, file,
73 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
73 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
74 select as raw_select, stylesheet_link, submit, text, password, textarea,
74 select as raw_select, stylesheet_link, submit, text, password, textarea,
75 ul, radio, Options)
75 ul, radio, Options)
76
76
77 from webhelpers2.number import format_byte_size
77 from webhelpers2.number import format_byte_size
78
78
79 from rhodecode.lib.action_parser import action_parser
79 from rhodecode.lib.action_parser import action_parser
80 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
80 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
81 from rhodecode.lib.ext_json import json
81 from rhodecode.lib.ext_json import json
82 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
82 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
83 from rhodecode.lib.utils2 import (
83 from rhodecode.lib.utils2 import (
84 str2bool, safe_unicode, safe_str,
84 str2bool, safe_unicode, safe_str,
85 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
85 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
86 AttributeDict, safe_int, md5, md5_safe, get_host_info)
86 AttributeDict, safe_int, md5, md5_safe, get_host_info)
87 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
87 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
88 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
88 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
89 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
89 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
90 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
90 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
91 from rhodecode.lib.index.search_utils import get_matching_line_offsets
91 from rhodecode.lib.index.search_utils import get_matching_line_offsets
92 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
92 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
93 from rhodecode.model.changeset_status import ChangesetStatusModel
93 from rhodecode.model.changeset_status import ChangesetStatusModel
94 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
94 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
95 from rhodecode.model.repo_group import RepoGroupModel
95 from rhodecode.model.repo_group import RepoGroupModel
96 from rhodecode.model.settings import IssueTrackerSettingsModel
96 from rhodecode.model.settings import IssueTrackerSettingsModel
97
97
98
98
99 log = logging.getLogger(__name__)
99 log = logging.getLogger(__name__)
100
100
101
101
102 DEFAULT_USER = User.DEFAULT_USER
102 DEFAULT_USER = User.DEFAULT_USER
103 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
103 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
104
104
105
105
106 def asset(path, ver=None, **kwargs):
106 def asset(path, ver=None, **kwargs):
107 """
107 """
108 Helper to generate a static asset file path for rhodecode assets
108 Helper to generate a static asset file path for rhodecode assets
109
109
110 eg. h.asset('images/image.png', ver='3923')
110 eg. h.asset('images/image.png', ver='3923')
111
111
112 :param path: path of asset
112 :param path: path of asset
113 :param ver: optional version query param to append as ?ver=
113 :param ver: optional version query param to append as ?ver=
114 """
114 """
115 request = get_current_request()
115 request = get_current_request()
116 query = {}
116 query = {}
117 query.update(kwargs)
117 query.update(kwargs)
118 if ver:
118 if ver:
119 query = {'ver': ver}
119 query = {'ver': ver}
120 return request.static_path(
120 return request.static_path(
121 'rhodecode:public/{}'.format(path), _query=query)
121 'rhodecode:public/{}'.format(path), _query=query)
122
122
123
123
124 default_html_escape_table = {
124 default_html_escape_table = {
125 ord('&'): u'&amp;',
125 ord('&'): u'&amp;',
126 ord('<'): u'&lt;',
126 ord('<'): u'&lt;',
127 ord('>'): u'&gt;',
127 ord('>'): u'&gt;',
128 ord('"'): u'&quot;',
128 ord('"'): u'&quot;',
129 ord("'"): u'&#39;',
129 ord("'"): u'&#39;',
130 }
130 }
131
131
132
132
133 def html_escape(text, html_escape_table=default_html_escape_table):
133 def html_escape(text, html_escape_table=default_html_escape_table):
134 """Produce entities within text."""
134 """Produce entities within text."""
135 return text.translate(html_escape_table)
135 return text.translate(html_escape_table)
136
136
137
137
138 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
138 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
139 """
139 """
140 Truncate string ``s`` at the first occurrence of ``sub``.
140 Truncate string ``s`` at the first occurrence of ``sub``.
141
141
142 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
142 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
143 """
143 """
144 suffix_if_chopped = suffix_if_chopped or ''
144 suffix_if_chopped = suffix_if_chopped or ''
145 pos = s.find(sub)
145 pos = s.find(sub)
146 if pos == -1:
146 if pos == -1:
147 return s
147 return s
148
148
149 if inclusive:
149 if inclusive:
150 pos += len(sub)
150 pos += len(sub)
151
151
152 chopped = s[:pos]
152 chopped = s[:pos]
153 left = s[pos:].strip()
153 left = s[pos:].strip()
154
154
155 if left and suffix_if_chopped:
155 if left and suffix_if_chopped:
156 chopped += suffix_if_chopped
156 chopped += suffix_if_chopped
157
157
158 return chopped
158 return chopped
159
159
160
160
161 def shorter(text, size=20, prefix=False):
161 def shorter(text, size=20, prefix=False):
162 postfix = '...'
162 postfix = '...'
163 if len(text) > size:
163 if len(text) > size:
164 if prefix:
164 if prefix:
165 # shorten in front
165 # shorten in front
166 return postfix + text[-(size - len(postfix)):]
166 return postfix + text[-(size - len(postfix)):]
167 else:
167 else:
168 return text[:size - len(postfix)] + postfix
168 return text[:size - len(postfix)] + postfix
169 return text
169 return text
170
170
171
171
172 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
172 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
173 """
173 """
174 Reset button
174 Reset button
175 """
175 """
176 return _input(type, name, value, id, attrs)
176 return _input(type, name, value, id, attrs)
177
177
178
178
179 def select(name, selected_values, options, id=NotGiven, **attrs):
179 def select(name, selected_values, options, id=NotGiven, **attrs):
180
180
181 if isinstance(options, (list, tuple)):
181 if isinstance(options, (list, tuple)):
182 options_iter = options
182 options_iter = options
183 # Handle old value,label lists ... where value also can be value,label lists
183 # Handle old value,label lists ... where value also can be value,label lists
184 options = Options()
184 options = Options()
185 for opt in options_iter:
185 for opt in options_iter:
186 if isinstance(opt, tuple) and len(opt) == 2:
186 if isinstance(opt, tuple) and len(opt) == 2:
187 value, label = opt
187 value, label = opt
188 elif isinstance(opt, basestring):
188 elif isinstance(opt, basestring):
189 value = label = opt
189 value = label = opt
190 else:
190 else:
191 raise ValueError('invalid select option type %r' % type(opt))
191 raise ValueError('invalid select option type %r' % type(opt))
192
192
193 if isinstance(value, (list, tuple)):
193 if isinstance(value, (list, tuple)):
194 option_group = options.add_optgroup(label)
194 option_group = options.add_optgroup(label)
195 for opt2 in value:
195 for opt2 in value:
196 if isinstance(opt2, tuple) and len(opt2) == 2:
196 if isinstance(opt2, tuple) and len(opt2) == 2:
197 group_value, group_label = opt2
197 group_value, group_label = opt2
198 elif isinstance(opt2, basestring):
198 elif isinstance(opt2, basestring):
199 group_value = group_label = opt2
199 group_value = group_label = opt2
200 else:
200 else:
201 raise ValueError('invalid select option type %r' % type(opt2))
201 raise ValueError('invalid select option type %r' % type(opt2))
202
202
203 option_group.add_option(group_label, group_value)
203 option_group.add_option(group_label, group_value)
204 else:
204 else:
205 options.add_option(label, value)
205 options.add_option(label, value)
206
206
207 return raw_select(name, selected_values, options, id=id, **attrs)
207 return raw_select(name, selected_values, options, id=id, **attrs)
208
208
209
209
210 def branding(name, length=40):
210 def branding(name, length=40):
211 return truncate(name, length, indicator="")
211 return truncate(name, length, indicator="")
212
212
213
213
214 def FID(raw_id, path):
214 def FID(raw_id, path):
215 """
215 """
216 Creates a unique ID for filenode based on it's hash of path and commit
216 Creates a unique ID for filenode based on it's hash of path and commit
217 it's safe to use in urls
217 it's safe to use in urls
218
218
219 :param raw_id:
219 :param raw_id:
220 :param path:
220 :param path:
221 """
221 """
222
222
223 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
223 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
224
224
225
225
226 class _GetError(object):
226 class _GetError(object):
227 """Get error from form_errors, and represent it as span wrapped error
227 """Get error from form_errors, and represent it as span wrapped error
228 message
228 message
229
229
230 :param field_name: field to fetch errors for
230 :param field_name: field to fetch errors for
231 :param form_errors: form errors dict
231 :param form_errors: form errors dict
232 """
232 """
233
233
234 def __call__(self, field_name, form_errors):
234 def __call__(self, field_name, form_errors):
235 tmpl = """<span class="error_msg">%s</span>"""
235 tmpl = """<span class="error_msg">%s</span>"""
236 if form_errors and field_name in form_errors:
236 if form_errors and field_name in form_errors:
237 return literal(tmpl % form_errors.get(field_name))
237 return literal(tmpl % form_errors.get(field_name))
238
238
239
239
240 get_error = _GetError()
240 get_error = _GetError()
241
241
242
242
243 class _ToolTip(object):
243 class _ToolTip(object):
244
244
245 def __call__(self, tooltip_title, trim_at=50):
245 def __call__(self, tooltip_title, trim_at=50):
246 """
246 """
247 Special function just to wrap our text into nice formatted
247 Special function just to wrap our text into nice formatted
248 autowrapped text
248 autowrapped text
249
249
250 :param tooltip_title:
250 :param tooltip_title:
251 """
251 """
252 tooltip_title = escape(tooltip_title)
252 tooltip_title = escape(tooltip_title)
253 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
253 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
254 return tooltip_title
254 return tooltip_title
255
255
256
256
257 tooltip = _ToolTip()
257 tooltip = _ToolTip()
258
258
259 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
259 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
260
260
261
261
262 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
262 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
263 limit_items=False, linkify_last_item=False, hide_last_item=False,
263 limit_items=False, linkify_last_item=False, hide_last_item=False,
264 copy_path_icon=True):
264 copy_path_icon=True):
265 if isinstance(file_path, str):
265 if isinstance(file_path, str):
266 file_path = safe_unicode(file_path)
266 file_path = safe_unicode(file_path)
267
267
268 if at_ref:
268 if at_ref:
269 route_qry = {'at': at_ref}
269 route_qry = {'at': at_ref}
270 default_landing_ref = at_ref or landing_ref_name or commit_id
270 default_landing_ref = at_ref or landing_ref_name or commit_id
271 else:
271 else:
272 route_qry = None
272 route_qry = None
273 default_landing_ref = commit_id
273 default_landing_ref = commit_id
274
274
275 # first segment is a `HOME` link to repo files root location
275 # first segment is a `HOME` link to repo files root location
276 root_name = literal(u'<i class="icon-home"></i>')
276 root_name = literal(u'<i class="icon-home"></i>')
277
277
278 url_segments = [
278 url_segments = [
279 link_to(
279 link_to(
280 root_name,
280 root_name,
281 repo_files_by_ref_url(
281 repo_files_by_ref_url(
282 repo_name,
282 repo_name,
283 repo_type,
283 repo_type,
284 f_path=None, # None here is a special case for SVN repos,
284 f_path=None, # None here is a special case for SVN repos,
285 # that won't prefix with a ref
285 # that won't prefix with a ref
286 ref_name=default_landing_ref,
286 ref_name=default_landing_ref,
287 commit_id=commit_id,
287 commit_id=commit_id,
288 query=route_qry
288 query=route_qry
289 )
289 )
290 )]
290 )]
291
291
292 path_segments = file_path.split('/')
292 path_segments = file_path.split('/')
293 last_cnt = len(path_segments) - 1
293 last_cnt = len(path_segments) - 1
294 for cnt, segment in enumerate(path_segments):
294 for cnt, segment in enumerate(path_segments):
295 if not segment:
295 if not segment:
296 continue
296 continue
297 segment_html = escape(segment)
297 segment_html = escape(segment)
298
298
299 last_item = cnt == last_cnt
299 last_item = cnt == last_cnt
300
300
301 if last_item and hide_last_item:
301 if last_item and hide_last_item:
302 # iterate over and hide last element
302 # iterate over and hide last element
303 continue
303 continue
304
304
305 if last_item and linkify_last_item is False:
305 if last_item and linkify_last_item is False:
306 # plain version
306 # plain version
307 url_segments.append(segment_html)
307 url_segments.append(segment_html)
308 else:
308 else:
309 url_segments.append(
309 url_segments.append(
310 link_to(
310 link_to(
311 segment_html,
311 segment_html,
312 repo_files_by_ref_url(
312 repo_files_by_ref_url(
313 repo_name,
313 repo_name,
314 repo_type,
314 repo_type,
315 f_path='/'.join(path_segments[:cnt + 1]),
315 f_path='/'.join(path_segments[:cnt + 1]),
316 ref_name=default_landing_ref,
316 ref_name=default_landing_ref,
317 commit_id=commit_id,
317 commit_id=commit_id,
318 query=route_qry
318 query=route_qry
319 ),
319 ),
320 ))
320 ))
321
321
322 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
322 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
323 if limit_items and len(limited_url_segments) < len(url_segments):
323 if limit_items and len(limited_url_segments) < len(url_segments):
324 url_segments = limited_url_segments
324 url_segments = limited_url_segments
325
325
326 full_path = file_path
326 full_path = file_path
327 if copy_path_icon:
327 if copy_path_icon:
328 icon = files_icon.format(escape(full_path))
328 icon = files_icon.format(escape(full_path))
329 else:
329 else:
330 icon = ''
330 icon = ''
331
331
332 if file_path == '':
332 if file_path == '':
333 return root_name
333 return root_name
334 else:
334 else:
335 return literal(' / '.join(url_segments) + icon)
335 return literal(' / '.join(url_segments) + icon)
336
336
337
337
338 def files_url_data(request):
338 def files_url_data(request):
339 matchdict = request.matchdict
339 matchdict = request.matchdict
340
340
341 if 'f_path' not in matchdict:
341 if 'f_path' not in matchdict:
342 matchdict['f_path'] = ''
342 matchdict['f_path'] = ''
343
343
344 if 'commit_id' not in matchdict:
344 if 'commit_id' not in matchdict:
345 matchdict['commit_id'] = 'tip'
345 matchdict['commit_id'] = 'tip'
346
346
347 return json.dumps(matchdict)
347 return json.dumps(matchdict)
348
348
349
349
350 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
350 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
351 _is_svn = is_svn(db_repo_type)
351 _is_svn = is_svn(db_repo_type)
352 final_f_path = f_path
352 final_f_path = f_path
353
353
354 if _is_svn:
354 if _is_svn:
355 """
355 """
356 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
356 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
357 actually commit_id followed by the ref_name. This should be done only in case
357 actually commit_id followed by the ref_name. This should be done only in case
358 This is a initial landing url, without additional paths.
358 This is a initial landing url, without additional paths.
359
359
360 like: /1000/tags/1.0.0/?at=tags/1.0.0
360 like: /1000/tags/1.0.0/?at=tags/1.0.0
361 """
361 """
362
362
363 if ref_name and ref_name != 'tip':
363 if ref_name and ref_name != 'tip':
364 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
364 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
365 # for SVN we only do this magic prefix if it's root, .eg landing revision
365 # for SVN we only do this magic prefix if it's root, .eg landing revision
366 # of files link. If we are in the tree we don't need this since we traverse the url
366 # of files link. If we are in the tree we don't need this since we traverse the url
367 # that has everything stored
367 # that has everything stored
368 if f_path in ['', '/']:
368 if f_path in ['', '/']:
369 final_f_path = '/'.join([ref_name, f_path])
369 final_f_path = '/'.join([ref_name, f_path])
370
370
371 # SVN always needs a commit_id explicitly, without a named REF
371 # SVN always needs a commit_id explicitly, without a named REF
372 default_commit_id = commit_id
372 default_commit_id = commit_id
373 else:
373 else:
374 """
374 """
375 For git and mercurial we construct a new URL using the names instead of commit_id
375 For git and mercurial we construct a new URL using the names instead of commit_id
376 like: /master/some_path?at=master
376 like: /master/some_path?at=master
377 """
377 """
378 # We currently do not support branches with slashes
378 # We currently do not support branches with slashes
379 if '/' in ref_name:
379 if '/' in ref_name:
380 default_commit_id = commit_id
380 default_commit_id = commit_id
381 else:
381 else:
382 default_commit_id = ref_name
382 default_commit_id = ref_name
383
383
384 # sometimes we pass f_path as None, to indicate explicit no prefix,
384 # sometimes we pass f_path as None, to indicate explicit no prefix,
385 # we translate it to string to not have None
385 # we translate it to string to not have None
386 final_f_path = final_f_path or ''
386 final_f_path = final_f_path or ''
387
387
388 files_url = route_path(
388 files_url = route_path(
389 'repo_files',
389 'repo_files',
390 repo_name=db_repo_name,
390 repo_name=db_repo_name,
391 commit_id=default_commit_id,
391 commit_id=default_commit_id,
392 f_path=final_f_path,
392 f_path=final_f_path,
393 _query=query
393 _query=query
394 )
394 )
395 return files_url
395 return files_url
396
396
397
397
398 def code_highlight(code, lexer, formatter, use_hl_filter=False):
398 def code_highlight(code, lexer, formatter, use_hl_filter=False):
399 """
399 """
400 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
400 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
401
401
402 If ``outfile`` is given and a valid file object (an object
402 If ``outfile`` is given and a valid file object (an object
403 with a ``write`` method), the result will be written to it, otherwise
403 with a ``write`` method), the result will be written to it, otherwise
404 it is returned as a string.
404 it is returned as a string.
405 """
405 """
406 if use_hl_filter:
406 if use_hl_filter:
407 # add HL filter
407 # add HL filter
408 from rhodecode.lib.index import search_utils
408 from rhodecode.lib.index import search_utils
409 lexer.add_filter(search_utils.ElasticSearchHLFilter())
409 lexer.add_filter(search_utils.ElasticSearchHLFilter())
410 return pygments.format(pygments.lex(code, lexer), formatter)
410 return pygments.format(pygments.lex(code, lexer), formatter)
411
411
412
412
413 class CodeHtmlFormatter(HtmlFormatter):
413 class CodeHtmlFormatter(HtmlFormatter):
414 """
414 """
415 My code Html Formatter for source codes
415 My code Html Formatter for source codes
416 """
416 """
417
417
418 def wrap(self, source, outfile):
418 def wrap(self, source, outfile):
419 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
419 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
420
420
421 def _wrap_code(self, source):
421 def _wrap_code(self, source):
422 for cnt, it in enumerate(source):
422 for cnt, it in enumerate(source):
423 i, t = it
423 i, t = it
424 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
424 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
425 yield i, t
425 yield i, t
426
426
427 def _wrap_tablelinenos(self, inner):
427 def _wrap_tablelinenos(self, inner):
428 dummyoutfile = StringIO.StringIO()
428 dummyoutfile = StringIO.StringIO()
429 lncount = 0
429 lncount = 0
430 for t, line in inner:
430 for t, line in inner:
431 if t:
431 if t:
432 lncount += 1
432 lncount += 1
433 dummyoutfile.write(line)
433 dummyoutfile.write(line)
434
434
435 fl = self.linenostart
435 fl = self.linenostart
436 mw = len(str(lncount + fl - 1))
436 mw = len(str(lncount + fl - 1))
437 sp = self.linenospecial
437 sp = self.linenospecial
438 st = self.linenostep
438 st = self.linenostep
439 la = self.lineanchors
439 la = self.lineanchors
440 aln = self.anchorlinenos
440 aln = self.anchorlinenos
441 nocls = self.noclasses
441 nocls = self.noclasses
442 if sp:
442 if sp:
443 lines = []
443 lines = []
444
444
445 for i in range(fl, fl + lncount):
445 for i in range(fl, fl + lncount):
446 if i % st == 0:
446 if i % st == 0:
447 if i % sp == 0:
447 if i % sp == 0:
448 if aln:
448 if aln:
449 lines.append('<a href="#%s%d" class="special">%*d</a>' %
449 lines.append('<a href="#%s%d" class="special">%*d</a>' %
450 (la, i, mw, i))
450 (la, i, mw, i))
451 else:
451 else:
452 lines.append('<span class="special">%*d</span>' % (mw, i))
452 lines.append('<span class="special">%*d</span>' % (mw, i))
453 else:
453 else:
454 if aln:
454 if aln:
455 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
455 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
456 else:
456 else:
457 lines.append('%*d' % (mw, i))
457 lines.append('%*d' % (mw, i))
458 else:
458 else:
459 lines.append('')
459 lines.append('')
460 ls = '\n'.join(lines)
460 ls = '\n'.join(lines)
461 else:
461 else:
462 lines = []
462 lines = []
463 for i in range(fl, fl + lncount):
463 for i in range(fl, fl + lncount):
464 if i % st == 0:
464 if i % st == 0:
465 if aln:
465 if aln:
466 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
466 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
467 else:
467 else:
468 lines.append('%*d' % (mw, i))
468 lines.append('%*d' % (mw, i))
469 else:
469 else:
470 lines.append('')
470 lines.append('')
471 ls = '\n'.join(lines)
471 ls = '\n'.join(lines)
472
472
473 # in case you wonder about the seemingly redundant <div> here: since the
473 # in case you wonder about the seemingly redundant <div> here: since the
474 # content in the other cell also is wrapped in a div, some browsers in
474 # content in the other cell also is wrapped in a div, some browsers in
475 # some configurations seem to mess up the formatting...
475 # some configurations seem to mess up the formatting...
476 if nocls:
476 if nocls:
477 yield 0, ('<table class="%stable">' % self.cssclass +
477 yield 0, ('<table class="%stable">' % self.cssclass +
478 '<tr><td><div class="linenodiv" '
478 '<tr><td><div class="linenodiv" '
479 'style="background-color: #f0f0f0; padding-right: 10px">'
479 'style="background-color: #f0f0f0; padding-right: 10px">'
480 '<pre style="line-height: 125%">' +
480 '<pre style="line-height: 125%">' +
481 ls + '</pre></div></td><td id="hlcode" class="code">')
481 ls + '</pre></div></td><td id="hlcode" class="code">')
482 else:
482 else:
483 yield 0, ('<table class="%stable">' % self.cssclass +
483 yield 0, ('<table class="%stable">' % self.cssclass +
484 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
484 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
485 ls + '</pre></div></td><td id="hlcode" class="code">')
485 ls + '</pre></div></td><td id="hlcode" class="code">')
486 yield 0, dummyoutfile.getvalue()
486 yield 0, dummyoutfile.getvalue()
487 yield 0, '</td></tr></table>'
487 yield 0, '</td></tr></table>'
488
488
489
489
490 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
490 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
491 def __init__(self, **kw):
491 def __init__(self, **kw):
492 # only show these line numbers if set
492 # only show these line numbers if set
493 self.only_lines = kw.pop('only_line_numbers', [])
493 self.only_lines = kw.pop('only_line_numbers', [])
494 self.query_terms = kw.pop('query_terms', [])
494 self.query_terms = kw.pop('query_terms', [])
495 self.max_lines = kw.pop('max_lines', 5)
495 self.max_lines = kw.pop('max_lines', 5)
496 self.line_context = kw.pop('line_context', 3)
496 self.line_context = kw.pop('line_context', 3)
497 self.url = kw.pop('url', None)
497 self.url = kw.pop('url', None)
498
498
499 super(CodeHtmlFormatter, self).__init__(**kw)
499 super(CodeHtmlFormatter, self).__init__(**kw)
500
500
501 def _wrap_code(self, source):
501 def _wrap_code(self, source):
502 for cnt, it in enumerate(source):
502 for cnt, it in enumerate(source):
503 i, t = it
503 i, t = it
504 t = '<pre>%s</pre>' % t
504 t = '<pre>%s</pre>' % t
505 yield i, t
505 yield i, t
506
506
507 def _wrap_tablelinenos(self, inner):
507 def _wrap_tablelinenos(self, inner):
508 yield 0, '<table class="code-highlight %stable">' % self.cssclass
508 yield 0, '<table class="code-highlight %stable">' % self.cssclass
509
509
510 last_shown_line_number = 0
510 last_shown_line_number = 0
511 current_line_number = 1
511 current_line_number = 1
512
512
513 for t, line in inner:
513 for t, line in inner:
514 if not t:
514 if not t:
515 yield t, line
515 yield t, line
516 continue
516 continue
517
517
518 if current_line_number in self.only_lines:
518 if current_line_number in self.only_lines:
519 if last_shown_line_number + 1 != current_line_number:
519 if last_shown_line_number + 1 != current_line_number:
520 yield 0, '<tr>'
520 yield 0, '<tr>'
521 yield 0, '<td class="line">...</td>'
521 yield 0, '<td class="line">...</td>'
522 yield 0, '<td id="hlcode" class="code"></td>'
522 yield 0, '<td id="hlcode" class="code"></td>'
523 yield 0, '</tr>'
523 yield 0, '</tr>'
524
524
525 yield 0, '<tr>'
525 yield 0, '<tr>'
526 if self.url:
526 if self.url:
527 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
527 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
528 self.url, current_line_number, current_line_number)
528 self.url, current_line_number, current_line_number)
529 else:
529 else:
530 yield 0, '<td class="line"><a href="">%i</a></td>' % (
530 yield 0, '<td class="line"><a href="">%i</a></td>' % (
531 current_line_number)
531 current_line_number)
532 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
532 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
533 yield 0, '</tr>'
533 yield 0, '</tr>'
534
534
535 last_shown_line_number = current_line_number
535 last_shown_line_number = current_line_number
536
536
537 current_line_number += 1
537 current_line_number += 1
538
538
539 yield 0, '</table>'
539 yield 0, '</table>'
540
540
541
541
542 def hsv_to_rgb(h, s, v):
542 def hsv_to_rgb(h, s, v):
543 """ Convert hsv color values to rgb """
543 """ Convert hsv color values to rgb """
544
544
545 if s == 0.0:
545 if s == 0.0:
546 return v, v, v
546 return v, v, v
547 i = int(h * 6.0) # XXX assume int() truncates!
547 i = int(h * 6.0) # XXX assume int() truncates!
548 f = (h * 6.0) - i
548 f = (h * 6.0) - i
549 p = v * (1.0 - s)
549 p = v * (1.0 - s)
550 q = v * (1.0 - s * f)
550 q = v * (1.0 - s * f)
551 t = v * (1.0 - s * (1.0 - f))
551 t = v * (1.0 - s * (1.0 - f))
552 i = i % 6
552 i = i % 6
553 if i == 0:
553 if i == 0:
554 return v, t, p
554 return v, t, p
555 if i == 1:
555 if i == 1:
556 return q, v, p
556 return q, v, p
557 if i == 2:
557 if i == 2:
558 return p, v, t
558 return p, v, t
559 if i == 3:
559 if i == 3:
560 return p, q, v
560 return p, q, v
561 if i == 4:
561 if i == 4:
562 return t, p, v
562 return t, p, v
563 if i == 5:
563 if i == 5:
564 return v, p, q
564 return v, p, q
565
565
566
566
567 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
567 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
568 """
568 """
569 Generator for getting n of evenly distributed colors using
569 Generator for getting n of evenly distributed colors using
570 hsv color and golden ratio. It always return same order of colors
570 hsv color and golden ratio. It always return same order of colors
571
571
572 :param n: number of colors to generate
572 :param n: number of colors to generate
573 :param saturation: saturation of returned colors
573 :param saturation: saturation of returned colors
574 :param lightness: lightness of returned colors
574 :param lightness: lightness of returned colors
575 :returns: RGB tuple
575 :returns: RGB tuple
576 """
576 """
577
577
578 golden_ratio = 0.618033988749895
578 golden_ratio = 0.618033988749895
579 h = 0.22717784590367374
579 h = 0.22717784590367374
580
580
581 for _ in xrange(n):
581 for _ in xrange(n):
582 h += golden_ratio
582 h += golden_ratio
583 h %= 1
583 h %= 1
584 HSV_tuple = [h, saturation, lightness]
584 HSV_tuple = [h, saturation, lightness]
585 RGB_tuple = hsv_to_rgb(*HSV_tuple)
585 RGB_tuple = hsv_to_rgb(*HSV_tuple)
586 yield map(lambda x: str(int(x * 256)), RGB_tuple)
586 yield map(lambda x: str(int(x * 256)), RGB_tuple)
587
587
588
588
589 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
589 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
590 """
590 """
591 Returns a function which when called with an argument returns a unique
591 Returns a function which when called with an argument returns a unique
592 color for that argument, eg.
592 color for that argument, eg.
593
593
594 :param n: number of colors to generate
594 :param n: number of colors to generate
595 :param saturation: saturation of returned colors
595 :param saturation: saturation of returned colors
596 :param lightness: lightness of returned colors
596 :param lightness: lightness of returned colors
597 :returns: css RGB string
597 :returns: css RGB string
598
598
599 >>> color_hash = color_hasher()
599 >>> color_hash = color_hasher()
600 >>> color_hash('hello')
600 >>> color_hash('hello')
601 'rgb(34, 12, 59)'
601 'rgb(34, 12, 59)'
602 >>> color_hash('hello')
602 >>> color_hash('hello')
603 'rgb(34, 12, 59)'
603 'rgb(34, 12, 59)'
604 >>> color_hash('other')
604 >>> color_hash('other')
605 'rgb(90, 224, 159)'
605 'rgb(90, 224, 159)'
606 """
606 """
607
607
608 color_dict = {}
608 color_dict = {}
609 cgenerator = unique_color_generator(
609 cgenerator = unique_color_generator(
610 saturation=saturation, lightness=lightness)
610 saturation=saturation, lightness=lightness)
611
611
612 def get_color_string(thing):
612 def get_color_string(thing):
613 if thing in color_dict:
613 if thing in color_dict:
614 col = color_dict[thing]
614 col = color_dict[thing]
615 else:
615 else:
616 col = color_dict[thing] = cgenerator.next()
616 col = color_dict[thing] = cgenerator.next()
617 return "rgb(%s)" % (', '.join(col))
617 return "rgb(%s)" % (', '.join(col))
618
618
619 return get_color_string
619 return get_color_string
620
620
621
621
622 def get_lexer_safe(mimetype=None, filepath=None):
622 def get_lexer_safe(mimetype=None, filepath=None):
623 """
623 """
624 Tries to return a relevant pygments lexer using mimetype/filepath name,
624 Tries to return a relevant pygments lexer using mimetype/filepath name,
625 defaulting to plain text if none could be found
625 defaulting to plain text if none could be found
626 """
626 """
627 lexer = None
627 lexer = None
628 try:
628 try:
629 if mimetype:
629 if mimetype:
630 lexer = get_lexer_for_mimetype(mimetype)
630 lexer = get_lexer_for_mimetype(mimetype)
631 if not lexer:
631 if not lexer:
632 lexer = get_lexer_for_filename(filepath)
632 lexer = get_lexer_for_filename(filepath)
633 except pygments.util.ClassNotFound:
633 except pygments.util.ClassNotFound:
634 pass
634 pass
635
635
636 if not lexer:
636 if not lexer:
637 lexer = get_lexer_by_name('text')
637 lexer = get_lexer_by_name('text')
638
638
639 return lexer
639 return lexer
640
640
641
641
642 def get_lexer_for_filenode(filenode):
642 def get_lexer_for_filenode(filenode):
643 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
643 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
644 return lexer
644 return lexer
645
645
646
646
647 def pygmentize(filenode, **kwargs):
647 def pygmentize(filenode, **kwargs):
648 """
648 """
649 pygmentize function using pygments
649 pygmentize function using pygments
650
650
651 :param filenode:
651 :param filenode:
652 """
652 """
653 lexer = get_lexer_for_filenode(filenode)
653 lexer = get_lexer_for_filenode(filenode)
654 return literal(code_highlight(filenode.content, lexer,
654 return literal(code_highlight(filenode.content, lexer,
655 CodeHtmlFormatter(**kwargs)))
655 CodeHtmlFormatter(**kwargs)))
656
656
657
657
658 def is_following_repo(repo_name, user_id):
658 def is_following_repo(repo_name, user_id):
659 from rhodecode.model.scm import ScmModel
659 from rhodecode.model.scm import ScmModel
660 return ScmModel().is_following_repo(repo_name, user_id)
660 return ScmModel().is_following_repo(repo_name, user_id)
661
661
662
662
663 class _Message(object):
663 class _Message(object):
664 """A message returned by ``Flash.pop_messages()``.
664 """A message returned by ``Flash.pop_messages()``.
665
665
666 Converting the message to a string returns the message text. Instances
666 Converting the message to a string returns the message text. Instances
667 also have the following attributes:
667 also have the following attributes:
668
668
669 * ``message``: the message text.
669 * ``message``: the message text.
670 * ``category``: the category specified when the message was created.
670 * ``category``: the category specified when the message was created.
671 """
671 """
672
672
673 def __init__(self, category, message, sub_data=None):
673 def __init__(self, category, message, sub_data=None):
674 self.category = category
674 self.category = category
675 self.message = message
675 self.message = message
676 self.sub_data = sub_data or {}
676 self.sub_data = sub_data or {}
677
677
678 def __str__(self):
678 def __str__(self):
679 return self.message
679 return self.message
680
680
681 __unicode__ = __str__
681 __unicode__ = __str__
682
682
683 def __html__(self):
683 def __html__(self):
684 return escape(safe_unicode(self.message))
684 return escape(safe_unicode(self.message))
685
685
686
686
687 class Flash(object):
687 class Flash(object):
688 # List of allowed categories. If None, allow any category.
688 # List of allowed categories. If None, allow any category.
689 categories = ["warning", "notice", "error", "success"]
689 categories = ["warning", "notice", "error", "success"]
690
690
691 # Default category if none is specified.
691 # Default category if none is specified.
692 default_category = "notice"
692 default_category = "notice"
693
693
694 def __init__(self, session_key="flash", categories=None,
694 def __init__(self, session_key="flash", categories=None,
695 default_category=None):
695 default_category=None):
696 """
696 """
697 Instantiate a ``Flash`` object.
697 Instantiate a ``Flash`` object.
698
698
699 ``session_key`` is the key to save the messages under in the user's
699 ``session_key`` is the key to save the messages under in the user's
700 session.
700 session.
701
701
702 ``categories`` is an optional list which overrides the default list
702 ``categories`` is an optional list which overrides the default list
703 of categories.
703 of categories.
704
704
705 ``default_category`` overrides the default category used for messages
705 ``default_category`` overrides the default category used for messages
706 when none is specified.
706 when none is specified.
707 """
707 """
708 self.session_key = session_key
708 self.session_key = session_key
709 if categories is not None:
709 if categories is not None:
710 self.categories = categories
710 self.categories = categories
711 if default_category is not None:
711 if default_category is not None:
712 self.default_category = default_category
712 self.default_category = default_category
713 if self.categories and self.default_category not in self.categories:
713 if self.categories and self.default_category not in self.categories:
714 raise ValueError(
714 raise ValueError(
715 "unrecognized default category %r" % (self.default_category,))
715 "unrecognized default category %r" % (self.default_category,))
716
716
717 def pop_messages(self, session=None, request=None):
717 def pop_messages(self, session=None, request=None):
718 """
718 """
719 Return all accumulated messages and delete them from the session.
719 Return all accumulated messages and delete them from the session.
720
720
721 The return value is a list of ``Message`` objects.
721 The return value is a list of ``Message`` objects.
722 """
722 """
723 messages = []
723 messages = []
724
724
725 if not session:
725 if not session:
726 if not request:
726 if not request:
727 request = get_current_request()
727 request = get_current_request()
728 session = request.session
728 session = request.session
729
729
730 # Pop the 'old' pylons flash messages. They are tuples of the form
730 # Pop the 'old' pylons flash messages. They are tuples of the form
731 # (category, message)
731 # (category, message)
732 for cat, msg in session.pop(self.session_key, []):
732 for cat, msg in session.pop(self.session_key, []):
733 messages.append(_Message(cat, msg))
733 messages.append(_Message(cat, msg))
734
734
735 # Pop the 'new' pyramid flash messages for each category as list
735 # Pop the 'new' pyramid flash messages for each category as list
736 # of strings.
736 # of strings.
737 for cat in self.categories:
737 for cat in self.categories:
738 for msg in session.pop_flash(queue=cat):
738 for msg in session.pop_flash(queue=cat):
739 sub_data = {}
739 sub_data = {}
740 if hasattr(msg, 'rsplit'):
740 if hasattr(msg, 'rsplit'):
741 flash_data = msg.rsplit('|DELIM|', 1)
741 flash_data = msg.rsplit('|DELIM|', 1)
742 org_message = flash_data[0]
742 org_message = flash_data[0]
743 if len(flash_data) > 1:
743 if len(flash_data) > 1:
744 sub_data = json.loads(flash_data[1])
744 sub_data = json.loads(flash_data[1])
745 else:
745 else:
746 org_message = msg
746 org_message = msg
747
747
748 messages.append(_Message(cat, org_message, sub_data=sub_data))
748 messages.append(_Message(cat, org_message, sub_data=sub_data))
749
749
750 # Map messages from the default queue to the 'notice' category.
750 # Map messages from the default queue to the 'notice' category.
751 for msg in session.pop_flash():
751 for msg in session.pop_flash():
752 messages.append(_Message('notice', msg))
752 messages.append(_Message('notice', msg))
753
753
754 session.save()
754 session.save()
755 return messages
755 return messages
756
756
757 def json_alerts(self, session=None, request=None):
757 def json_alerts(self, session=None, request=None):
758 payloads = []
758 payloads = []
759 messages = flash.pop_messages(session=session, request=request) or []
759 messages = flash.pop_messages(session=session, request=request) or []
760 for message in messages:
760 for message in messages:
761 payloads.append({
761 payloads.append({
762 'message': {
762 'message': {
763 'message': u'{}'.format(message.message),
763 'message': u'{}'.format(message.message),
764 'level': message.category,
764 'level': message.category,
765 'force': True,
765 'force': True,
766 'subdata': message.sub_data
766 'subdata': message.sub_data
767 }
767 }
768 })
768 })
769 return json.dumps(payloads)
769 return json.dumps(payloads)
770
770
771 def __call__(self, message, category=None, ignore_duplicate=True,
771 def __call__(self, message, category=None, ignore_duplicate=True,
772 session=None, request=None):
772 session=None, request=None):
773
773
774 if not session:
774 if not session:
775 if not request:
775 if not request:
776 request = get_current_request()
776 request = get_current_request()
777 session = request.session
777 session = request.session
778
778
779 session.flash(
779 session.flash(
780 message, queue=category, allow_duplicate=not ignore_duplicate)
780 message, queue=category, allow_duplicate=not ignore_duplicate)
781
781
782
782
783 flash = Flash()
783 flash = Flash()
784
784
785 #==============================================================================
785 #==============================================================================
786 # SCM FILTERS available via h.
786 # SCM FILTERS available via h.
787 #==============================================================================
787 #==============================================================================
788 from rhodecode.lib.vcs.utils import author_name, author_email
788 from rhodecode.lib.vcs.utils import author_name, author_email
789 from rhodecode.lib.utils2 import age, age_from_seconds
789 from rhodecode.lib.utils2 import age, age_from_seconds
790 from rhodecode.model.db import User, ChangesetStatus
790 from rhodecode.model.db import User, ChangesetStatus
791
791
792
792
793 email = author_email
793 email = author_email
794
794
795
795
796 def capitalize(raw_text):
796 def capitalize(raw_text):
797 return raw_text.capitalize()
797 return raw_text.capitalize()
798
798
799
799
800 def short_id(long_id):
800 def short_id(long_id):
801 return long_id[:12]
801 return long_id[:12]
802
802
803
803
804 def hide_credentials(url):
804 def hide_credentials(url):
805 from rhodecode.lib.utils2 import credentials_filter
805 from rhodecode.lib.utils2 import credentials_filter
806 return credentials_filter(url)
806 return credentials_filter(url)
807
807
808
808
809 import pytz
809 import pytz
810 import tzlocal
810 import tzlocal
811 local_timezone = tzlocal.get_localzone()
811 local_timezone = tzlocal.get_localzone()
812
812
813
813
814 def get_timezone(datetime_iso, time_is_local=False):
814 def get_timezone(datetime_iso, time_is_local=False):
815 tzinfo = '+00:00'
815 tzinfo = '+00:00'
816
816
817 # detect if we have a timezone info, otherwise, add it
817 # detect if we have a timezone info, otherwise, add it
818 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
818 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
819 force_timezone = os.environ.get('RC_TIMEZONE', '')
819 force_timezone = os.environ.get('RC_TIMEZONE', '')
820 if force_timezone:
820 if force_timezone:
821 force_timezone = pytz.timezone(force_timezone)
821 force_timezone = pytz.timezone(force_timezone)
822 timezone = force_timezone or local_timezone
822 timezone = force_timezone or local_timezone
823 offset = timezone.localize(datetime_iso).strftime('%z')
823 offset = timezone.localize(datetime_iso).strftime('%z')
824 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
824 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
825 return tzinfo
825 return tzinfo
826
826
827
827
828 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
828 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
829 title = value or format_date(datetime_iso)
829 title = value or format_date(datetime_iso)
830 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
830 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
831
831
832 return literal(
832 return literal(
833 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
833 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
834 cls='tooltip' if tooltip else '',
834 cls='tooltip' if tooltip else '',
835 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
835 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
836 title=title, dt=datetime_iso, tzinfo=tzinfo
836 title=title, dt=datetime_iso, tzinfo=tzinfo
837 ))
837 ))
838
838
839
839
840 def _shorten_commit_id(commit_id, commit_len=None):
840 def _shorten_commit_id(commit_id, commit_len=None):
841 if commit_len is None:
841 if commit_len is None:
842 request = get_current_request()
842 request = get_current_request()
843 commit_len = request.call_context.visual.show_sha_length
843 commit_len = request.call_context.visual.show_sha_length
844 return commit_id[:commit_len]
844 return commit_id[:commit_len]
845
845
846
846
847 def show_id(commit, show_idx=None, commit_len=None):
847 def show_id(commit, show_idx=None, commit_len=None):
848 """
848 """
849 Configurable function that shows ID
849 Configurable function that shows ID
850 by default it's r123:fffeeefffeee
850 by default it's r123:fffeeefffeee
851
851
852 :param commit: commit instance
852 :param commit: commit instance
853 """
853 """
854 if show_idx is None:
854 if show_idx is None:
855 request = get_current_request()
855 request = get_current_request()
856 show_idx = request.call_context.visual.show_revision_number
856 show_idx = request.call_context.visual.show_revision_number
857
857
858 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
858 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
859 if show_idx:
859 if show_idx:
860 return 'r%s:%s' % (commit.idx, raw_id)
860 return 'r%s:%s' % (commit.idx, raw_id)
861 else:
861 else:
862 return '%s' % (raw_id, )
862 return '%s' % (raw_id, )
863
863
864
864
865 def format_date(date):
865 def format_date(date):
866 """
866 """
867 use a standardized formatting for dates used in RhodeCode
867 use a standardized formatting for dates used in RhodeCode
868
868
869 :param date: date/datetime object
869 :param date: date/datetime object
870 :return: formatted date
870 :return: formatted date
871 """
871 """
872
872
873 if date:
873 if date:
874 _fmt = "%a, %d %b %Y %H:%M:%S"
874 _fmt = "%a, %d %b %Y %H:%M:%S"
875 return safe_unicode(date.strftime(_fmt))
875 return safe_unicode(date.strftime(_fmt))
876
876
877 return u""
877 return u""
878
878
879
879
880 class _RepoChecker(object):
880 class _RepoChecker(object):
881
881
882 def __init__(self, backend_alias):
882 def __init__(self, backend_alias):
883 self._backend_alias = backend_alias
883 self._backend_alias = backend_alias
884
884
885 def __call__(self, repository):
885 def __call__(self, repository):
886 if hasattr(repository, 'alias'):
886 if hasattr(repository, 'alias'):
887 _type = repository.alias
887 _type = repository.alias
888 elif hasattr(repository, 'repo_type'):
888 elif hasattr(repository, 'repo_type'):
889 _type = repository.repo_type
889 _type = repository.repo_type
890 else:
890 else:
891 _type = repository
891 _type = repository
892 return _type == self._backend_alias
892 return _type == self._backend_alias
893
893
894
894
895 is_git = _RepoChecker('git')
895 is_git = _RepoChecker('git')
896 is_hg = _RepoChecker('hg')
896 is_hg = _RepoChecker('hg')
897 is_svn = _RepoChecker('svn')
897 is_svn = _RepoChecker('svn')
898
898
899
899
900 def get_repo_type_by_name(repo_name):
900 def get_repo_type_by_name(repo_name):
901 repo = Repository.get_by_repo_name(repo_name)
901 repo = Repository.get_by_repo_name(repo_name)
902 if repo:
902 if repo:
903 return repo.repo_type
903 return repo.repo_type
904
904
905
905
906 def is_svn_without_proxy(repository):
906 def is_svn_without_proxy(repository):
907 if is_svn(repository):
907 if is_svn(repository):
908 from rhodecode.model.settings import VcsSettingsModel
908 from rhodecode.model.settings import VcsSettingsModel
909 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
909 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
910 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
910 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
911 return False
911 return False
912
912
913
913
914 def discover_user(author):
914 def discover_user(author):
915 """
915 """
916 Tries to discover RhodeCode User based on the author string. Author string
916 Tries to discover RhodeCode User based on the author string. Author string
917 is typically `FirstName LastName <email@address.com>`
917 is typically `FirstName LastName <email@address.com>`
918 """
918 """
919
919
920 # if author is already an instance use it for extraction
920 # if author is already an instance use it for extraction
921 if isinstance(author, User):
921 if isinstance(author, User):
922 return author
922 return author
923
923
924 # Valid email in the attribute passed, see if they're in the system
924 # Valid email in the attribute passed, see if they're in the system
925 _email = author_email(author)
925 _email = author_email(author)
926 if _email != '':
926 if _email != '':
927 user = User.get_by_email(_email, case_insensitive=True, cache=True)
927 user = User.get_by_email(_email, case_insensitive=True, cache=True)
928 if user is not None:
928 if user is not None:
929 return user
929 return user
930
930
931 # Maybe it's a username, we try to extract it and fetch by username ?
931 # Maybe it's a username, we try to extract it and fetch by username ?
932 _author = author_name(author)
932 _author = author_name(author)
933 user = User.get_by_username(_author, case_insensitive=True, cache=True)
933 user = User.get_by_username(_author, case_insensitive=True, cache=True)
934 if user is not None:
934 if user is not None:
935 return user
935 return user
936
936
937 return None
937 return None
938
938
939
939
940 def email_or_none(author):
940 def email_or_none(author):
941 # extract email from the commit string
941 # extract email from the commit string
942 _email = author_email(author)
942 _email = author_email(author)
943
943
944 # If we have an email, use it, otherwise
944 # If we have an email, use it, otherwise
945 # see if it contains a username we can get an email from
945 # see if it contains a username we can get an email from
946 if _email != '':
946 if _email != '':
947 return _email
947 return _email
948 else:
948 else:
949 user = User.get_by_username(
949 user = User.get_by_username(
950 author_name(author), case_insensitive=True, cache=True)
950 author_name(author), case_insensitive=True, cache=True)
951
951
952 if user is not None:
952 if user is not None:
953 return user.email
953 return user.email
954
954
955 # No valid email, not a valid user in the system, none!
955 # No valid email, not a valid user in the system, none!
956 return None
956 return None
957
957
958
958
959 def link_to_user(author, length=0, **kwargs):
959 def link_to_user(author, length=0, **kwargs):
960 user = discover_user(author)
960 user = discover_user(author)
961 # user can be None, but if we have it already it means we can re-use it
961 # user can be None, but if we have it already it means we can re-use it
962 # in the person() function, so we save 1 intensive-query
962 # in the person() function, so we save 1 intensive-query
963 if user:
963 if user:
964 author = user
964 author = user
965
965
966 display_person = person(author, 'username_or_name_or_email')
966 display_person = person(author, 'username_or_name_or_email')
967 if length:
967 if length:
968 display_person = shorter(display_person, length)
968 display_person = shorter(display_person, length)
969
969
970 if user and user.username != user.DEFAULT_USER:
970 if user and user.username != user.DEFAULT_USER:
971 return link_to(
971 return link_to(
972 escape(display_person),
972 escape(display_person),
973 route_path('user_profile', username=user.username),
973 route_path('user_profile', username=user.username),
974 **kwargs)
974 **kwargs)
975 else:
975 else:
976 return escape(display_person)
976 return escape(display_person)
977
977
978
978
979 def link_to_group(users_group_name, **kwargs):
979 def link_to_group(users_group_name, **kwargs):
980 return link_to(
980 return link_to(
981 escape(users_group_name),
981 escape(users_group_name),
982 route_path('user_group_profile', user_group_name=users_group_name),
982 route_path('user_group_profile', user_group_name=users_group_name),
983 **kwargs)
983 **kwargs)
984
984
985
985
986 def person(author, show_attr="username_and_name"):
986 def person(author, show_attr="username_and_name"):
987 user = discover_user(author)
987 user = discover_user(author)
988 if user:
988 if user:
989 return getattr(user, show_attr)
989 return getattr(user, show_attr)
990 else:
990 else:
991 _author = author_name(author)
991 _author = author_name(author)
992 _email = email(author)
992 _email = email(author)
993 return _author or _email
993 return _author or _email
994
994
995
995
996 def author_string(email):
996 def author_string(email):
997 if email:
997 if email:
998 user = User.get_by_email(email, case_insensitive=True, cache=True)
998 user = User.get_by_email(email, case_insensitive=True, cache=True)
999 if user:
999 if user:
1000 if user.first_name or user.last_name:
1000 if user.first_name or user.last_name:
1001 return '%s %s &lt;%s&gt;' % (
1001 return '%s %s &lt;%s&gt;' % (
1002 user.first_name, user.last_name, email)
1002 user.first_name, user.last_name, email)
1003 else:
1003 else:
1004 return email
1004 return email
1005 else:
1005 else:
1006 return email
1006 return email
1007 else:
1007 else:
1008 return None
1008 return None
1009
1009
1010
1010
1011 def person_by_id(id_, show_attr="username_and_name"):
1011 def person_by_id(id_, show_attr="username_and_name"):
1012 # attr to return from fetched user
1012 # attr to return from fetched user
1013 person_getter = lambda usr: getattr(usr, show_attr)
1013 person_getter = lambda usr: getattr(usr, show_attr)
1014
1014
1015 #maybe it's an ID ?
1015 #maybe it's an ID ?
1016 if str(id_).isdigit() or isinstance(id_, int):
1016 if str(id_).isdigit() or isinstance(id_, int):
1017 id_ = int(id_)
1017 id_ = int(id_)
1018 user = User.get(id_)
1018 user = User.get(id_)
1019 if user is not None:
1019 if user is not None:
1020 return person_getter(user)
1020 return person_getter(user)
1021 return id_
1021 return id_
1022
1022
1023
1023
1024 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1024 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1025 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1025 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1026 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1026 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1027
1027
1028
1028
1029 tags_paterns = OrderedDict((
1029 tags_paterns = OrderedDict((
1030 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1030 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1031 '<div class="metatag" tag="lang">\\2</div>')),
1031 '<div class="metatag" tag="lang">\\2</div>')),
1032
1032
1033 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1033 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1034 '<div class="metatag" tag="see">see: \\1 </div>')),
1034 '<div class="metatag" tag="see">see: \\1 </div>')),
1035
1035
1036 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1036 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1037 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1037 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1038
1038
1039 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1039 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1040 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1040 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1041
1041
1042 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1042 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1043 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1043 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1044
1044
1045 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1045 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1046 '<div class="metatag" tag="state \\1">\\1</div>')),
1046 '<div class="metatag" tag="state \\1">\\1</div>')),
1047
1047
1048 # label in grey
1048 # label in grey
1049 ('label', (re.compile(r'\[([a-z]+)\]'),
1049 ('label', (re.compile(r'\[([a-z]+)\]'),
1050 '<div class="metatag" tag="label">\\1</div>')),
1050 '<div class="metatag" tag="label">\\1</div>')),
1051
1051
1052 # generic catch all in grey
1052 # generic catch all in grey
1053 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1053 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1054 '<div class="metatag" tag="generic">\\1</div>')),
1054 '<div class="metatag" tag="generic">\\1</div>')),
1055 ))
1055 ))
1056
1056
1057
1057
1058 def extract_metatags(value):
1058 def extract_metatags(value):
1059 """
1059 """
1060 Extract supported meta-tags from given text value
1060 Extract supported meta-tags from given text value
1061 """
1061 """
1062 tags = []
1062 tags = []
1063 if not value:
1063 if not value:
1064 return tags, ''
1064 return tags, ''
1065
1065
1066 for key, val in tags_paterns.items():
1066 for key, val in tags_paterns.items():
1067 pat, replace_html = val
1067 pat, replace_html = val
1068 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1068 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1069 value = pat.sub('', value)
1069 value = pat.sub('', value)
1070
1070
1071 return tags, value
1071 return tags, value
1072
1072
1073
1073
1074 def style_metatag(tag_type, value):
1074 def style_metatag(tag_type, value):
1075 """
1075 """
1076 converts tags from value into html equivalent
1076 converts tags from value into html equivalent
1077 """
1077 """
1078 if not value:
1078 if not value:
1079 return ''
1079 return ''
1080
1080
1081 html_value = value
1081 html_value = value
1082 tag_data = tags_paterns.get(tag_type)
1082 tag_data = tags_paterns.get(tag_type)
1083 if tag_data:
1083 if tag_data:
1084 pat, replace_html = tag_data
1084 pat, replace_html = tag_data
1085 # convert to plain `unicode` instead of a markup tag to be used in
1085 # convert to plain `unicode` instead of a markup tag to be used in
1086 # regex expressions. safe_unicode doesn't work here
1086 # regex expressions. safe_unicode doesn't work here
1087 html_value = pat.sub(replace_html, unicode(value))
1087 html_value = pat.sub(replace_html, unicode(value))
1088
1088
1089 return html_value
1089 return html_value
1090
1090
1091
1091
1092 def bool2icon(value, show_at_false=True):
1092 def bool2icon(value, show_at_false=True):
1093 """
1093 """
1094 Returns boolean value of a given value, represented as html element with
1094 Returns boolean value of a given value, represented as html element with
1095 classes that will represent icons
1095 classes that will represent icons
1096
1096
1097 :param value: given value to convert to html node
1097 :param value: given value to convert to html node
1098 """
1098 """
1099
1099
1100 if value: # does bool conversion
1100 if value: # does bool conversion
1101 return HTML.tag('i', class_="icon-true", title='True')
1101 return HTML.tag('i', class_="icon-true", title='True')
1102 else: # not true as bool
1102 else: # not true as bool
1103 if show_at_false:
1103 if show_at_false:
1104 return HTML.tag('i', class_="icon-false", title='False')
1104 return HTML.tag('i', class_="icon-false", title='False')
1105 return HTML.tag('i')
1105 return HTML.tag('i')
1106
1106
1107
1108 def b64(inp):
1109 return base64.b64encode(inp)
1110
1107 #==============================================================================
1111 #==============================================================================
1108 # PERMS
1112 # PERMS
1109 #==============================================================================
1113 #==============================================================================
1110 from rhodecode.lib.auth import (
1114 from rhodecode.lib.auth import (
1111 HasPermissionAny, HasPermissionAll,
1115 HasPermissionAny, HasPermissionAll,
1112 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1116 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1113 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1117 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1114 csrf_token_key, AuthUser)
1118 csrf_token_key, AuthUser)
1115
1119
1116
1120
1117 #==============================================================================
1121 #==============================================================================
1118 # GRAVATAR URL
1122 # GRAVATAR URL
1119 #==============================================================================
1123 #==============================================================================
1120 class InitialsGravatar(object):
1124 class InitialsGravatar(object):
1121 def __init__(self, email_address, first_name, last_name, size=30,
1125 def __init__(self, email_address, first_name, last_name, size=30,
1122 background=None, text_color='#fff'):
1126 background=None, text_color='#fff'):
1123 self.size = size
1127 self.size = size
1124 self.first_name = first_name
1128 self.first_name = first_name
1125 self.last_name = last_name
1129 self.last_name = last_name
1126 self.email_address = email_address
1130 self.email_address = email_address
1127 self.background = background or self.str2color(email_address)
1131 self.background = background or self.str2color(email_address)
1128 self.text_color = text_color
1132 self.text_color = text_color
1129
1133
1130 def get_color_bank(self):
1134 def get_color_bank(self):
1131 """
1135 """
1132 returns a predefined list of colors that gravatars can use.
1136 returns a predefined list of colors that gravatars can use.
1133 Those are randomized distinct colors that guarantee readability and
1137 Those are randomized distinct colors that guarantee readability and
1134 uniqueness.
1138 uniqueness.
1135
1139
1136 generated with: http://phrogz.net/css/distinct-colors.html
1140 generated with: http://phrogz.net/css/distinct-colors.html
1137 """
1141 """
1138 return [
1142 return [
1139 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1143 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1140 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1144 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1141 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1145 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1142 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1146 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1143 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1147 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1144 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1148 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1145 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1149 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1146 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1150 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1147 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1151 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1148 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1152 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1149 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1153 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1150 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1154 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1151 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1155 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1152 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1156 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1153 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1157 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1154 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1158 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1155 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1159 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1156 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1160 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1157 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1161 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1158 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1162 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1159 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1163 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1160 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1164 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1161 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1165 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1162 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1166 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1163 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1167 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1164 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1168 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1165 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1169 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1166 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1170 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1167 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1171 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1168 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1172 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1169 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1173 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1170 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1174 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1171 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1175 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1172 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1176 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1173 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1177 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1174 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1178 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1175 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1179 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1176 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1180 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1177 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1181 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1178 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1182 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1179 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1183 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1180 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1184 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1181 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1185 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1182 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1186 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1183 '#4f8c46', '#368dd9', '#5c0073'
1187 '#4f8c46', '#368dd9', '#5c0073'
1184 ]
1188 ]
1185
1189
1186 def rgb_to_hex_color(self, rgb_tuple):
1190 def rgb_to_hex_color(self, rgb_tuple):
1187 """
1191 """
1188 Converts an rgb_tuple passed to an hex color.
1192 Converts an rgb_tuple passed to an hex color.
1189
1193
1190 :param rgb_tuple: tuple with 3 ints represents rgb color space
1194 :param rgb_tuple: tuple with 3 ints represents rgb color space
1191 """
1195 """
1192 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1196 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1193
1197
1194 def email_to_int_list(self, email_str):
1198 def email_to_int_list(self, email_str):
1195 """
1199 """
1196 Get every byte of the hex digest value of email and turn it to integer.
1200 Get every byte of the hex digest value of email and turn it to integer.
1197 It's going to be always between 0-255
1201 It's going to be always between 0-255
1198 """
1202 """
1199 digest = md5_safe(email_str.lower())
1203 digest = md5_safe(email_str.lower())
1200 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1204 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1201
1205
1202 def pick_color_bank_index(self, email_str, color_bank):
1206 def pick_color_bank_index(self, email_str, color_bank):
1203 return self.email_to_int_list(email_str)[0] % len(color_bank)
1207 return self.email_to_int_list(email_str)[0] % len(color_bank)
1204
1208
1205 def str2color(self, email_str):
1209 def str2color(self, email_str):
1206 """
1210 """
1207 Tries to map in a stable algorithm an email to color
1211 Tries to map in a stable algorithm an email to color
1208
1212
1209 :param email_str:
1213 :param email_str:
1210 """
1214 """
1211 color_bank = self.get_color_bank()
1215 color_bank = self.get_color_bank()
1212 # pick position (module it's length so we always find it in the
1216 # pick position (module it's length so we always find it in the
1213 # bank even if it's smaller than 256 values
1217 # bank even if it's smaller than 256 values
1214 pos = self.pick_color_bank_index(email_str, color_bank)
1218 pos = self.pick_color_bank_index(email_str, color_bank)
1215 return color_bank[pos]
1219 return color_bank[pos]
1216
1220
1217 def normalize_email(self, email_address):
1221 def normalize_email(self, email_address):
1218 import unicodedata
1222 import unicodedata
1219 # default host used to fill in the fake/missing email
1223 # default host used to fill in the fake/missing email
1220 default_host = u'localhost'
1224 default_host = u'localhost'
1221
1225
1222 if not email_address:
1226 if not email_address:
1223 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1227 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1224
1228
1225 email_address = safe_unicode(email_address)
1229 email_address = safe_unicode(email_address)
1226
1230
1227 if u'@' not in email_address:
1231 if u'@' not in email_address:
1228 email_address = u'%s@%s' % (email_address, default_host)
1232 email_address = u'%s@%s' % (email_address, default_host)
1229
1233
1230 if email_address.endswith(u'@'):
1234 if email_address.endswith(u'@'):
1231 email_address = u'%s%s' % (email_address, default_host)
1235 email_address = u'%s%s' % (email_address, default_host)
1232
1236
1233 email_address = unicodedata.normalize('NFKD', email_address)\
1237 email_address = unicodedata.normalize('NFKD', email_address)\
1234 .encode('ascii', 'ignore')
1238 .encode('ascii', 'ignore')
1235 return email_address
1239 return email_address
1236
1240
1237 def get_initials(self):
1241 def get_initials(self):
1238 """
1242 """
1239 Returns 2 letter initials calculated based on the input.
1243 Returns 2 letter initials calculated based on the input.
1240 The algorithm picks first given email address, and takes first letter
1244 The algorithm picks first given email address, and takes first letter
1241 of part before @, and then the first letter of server name. In case
1245 of part before @, and then the first letter of server name. In case
1242 the part before @ is in a format of `somestring.somestring2` it replaces
1246 the part before @ is in a format of `somestring.somestring2` it replaces
1243 the server letter with first letter of somestring2
1247 the server letter with first letter of somestring2
1244
1248
1245 In case function was initialized with both first and lastname, this
1249 In case function was initialized with both first and lastname, this
1246 overrides the extraction from email by first letter of the first and
1250 overrides the extraction from email by first letter of the first and
1247 last name. We add special logic to that functionality, In case Full name
1251 last name. We add special logic to that functionality, In case Full name
1248 is compound, like Guido Von Rossum, we use last part of the last name
1252 is compound, like Guido Von Rossum, we use last part of the last name
1249 (Von Rossum) picking `R`.
1253 (Von Rossum) picking `R`.
1250
1254
1251 Function also normalizes the non-ascii characters to they ascii
1255 Function also normalizes the non-ascii characters to they ascii
1252 representation, eg Ą => A
1256 representation, eg Ą => A
1253 """
1257 """
1254 import unicodedata
1258 import unicodedata
1255 # replace non-ascii to ascii
1259 # replace non-ascii to ascii
1256 first_name = unicodedata.normalize(
1260 first_name = unicodedata.normalize(
1257 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1261 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1258 last_name = unicodedata.normalize(
1262 last_name = unicodedata.normalize(
1259 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1263 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1260
1264
1261 # do NFKD encoding, and also make sure email has proper format
1265 # do NFKD encoding, and also make sure email has proper format
1262 email_address = self.normalize_email(self.email_address)
1266 email_address = self.normalize_email(self.email_address)
1263
1267
1264 # first push the email initials
1268 # first push the email initials
1265 prefix, server = email_address.split('@', 1)
1269 prefix, server = email_address.split('@', 1)
1266
1270
1267 # check if prefix is maybe a 'first_name.last_name' syntax
1271 # check if prefix is maybe a 'first_name.last_name' syntax
1268 _dot_split = prefix.rsplit('.', 1)
1272 _dot_split = prefix.rsplit('.', 1)
1269 if len(_dot_split) == 2 and _dot_split[1]:
1273 if len(_dot_split) == 2 and _dot_split[1]:
1270 initials = [_dot_split[0][0], _dot_split[1][0]]
1274 initials = [_dot_split[0][0], _dot_split[1][0]]
1271 else:
1275 else:
1272 initials = [prefix[0], server[0]]
1276 initials = [prefix[0], server[0]]
1273
1277
1274 # then try to replace either first_name or last_name
1278 # then try to replace either first_name or last_name
1275 fn_letter = (first_name or " ")[0].strip()
1279 fn_letter = (first_name or " ")[0].strip()
1276 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1280 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1277
1281
1278 if fn_letter:
1282 if fn_letter:
1279 initials[0] = fn_letter
1283 initials[0] = fn_letter
1280
1284
1281 if ln_letter:
1285 if ln_letter:
1282 initials[1] = ln_letter
1286 initials[1] = ln_letter
1283
1287
1284 return ''.join(initials).upper()
1288 return ''.join(initials).upper()
1285
1289
1286 def get_img_data_by_type(self, font_family, img_type):
1290 def get_img_data_by_type(self, font_family, img_type):
1287 default_user = """
1291 default_user = """
1288 <svg xmlns="http://www.w3.org/2000/svg"
1292 <svg xmlns="http://www.w3.org/2000/svg"
1289 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1293 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1290 viewBox="-15 -10 439.165 429.164"
1294 viewBox="-15 -10 439.165 429.164"
1291
1295
1292 xml:space="preserve"
1296 xml:space="preserve"
1293 style="background:{background};" >
1297 style="background:{background};" >
1294
1298
1295 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1299 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1296 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1300 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1297 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1301 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1298 168.596,153.916,216.671,
1302 168.596,153.916,216.671,
1299 204.583,216.671z" fill="{text_color}"/>
1303 204.583,216.671z" fill="{text_color}"/>
1300 <path d="M407.164,374.717L360.88,
1304 <path d="M407.164,374.717L360.88,
1301 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1305 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1302 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1306 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1303 15.366-44.203,23.488-69.076,23.488c-24.877,
1307 15.366-44.203,23.488-69.076,23.488c-24.877,
1304 0-48.762-8.122-69.078-23.488
1308 0-48.762-8.122-69.078-23.488
1305 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1309 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1306 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1310 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1307 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1311 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1308 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1312 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1309 19.402-10.527 C409.699,390.129,
1313 19.402-10.527 C409.699,390.129,
1310 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1314 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1311 </svg>""".format(
1315 </svg>""".format(
1312 size=self.size,
1316 size=self.size,
1313 background='#979797', # @grey4
1317 background='#979797', # @grey4
1314 text_color=self.text_color,
1318 text_color=self.text_color,
1315 font_family=font_family)
1319 font_family=font_family)
1316
1320
1317 return {
1321 return {
1318 "default_user": default_user
1322 "default_user": default_user
1319 }[img_type]
1323 }[img_type]
1320
1324
1321 def get_img_data(self, svg_type=None):
1325 def get_img_data(self, svg_type=None):
1322 """
1326 """
1323 generates the svg metadata for image
1327 generates the svg metadata for image
1324 """
1328 """
1325 fonts = [
1329 fonts = [
1326 '-apple-system',
1330 '-apple-system',
1327 'BlinkMacSystemFont',
1331 'BlinkMacSystemFont',
1328 'Segoe UI',
1332 'Segoe UI',
1329 'Roboto',
1333 'Roboto',
1330 'Oxygen-Sans',
1334 'Oxygen-Sans',
1331 'Ubuntu',
1335 'Ubuntu',
1332 'Cantarell',
1336 'Cantarell',
1333 'Helvetica Neue',
1337 'Helvetica Neue',
1334 'sans-serif'
1338 'sans-serif'
1335 ]
1339 ]
1336 font_family = ','.join(fonts)
1340 font_family = ','.join(fonts)
1337 if svg_type:
1341 if svg_type:
1338 return self.get_img_data_by_type(font_family, svg_type)
1342 return self.get_img_data_by_type(font_family, svg_type)
1339
1343
1340 initials = self.get_initials()
1344 initials = self.get_initials()
1341 img_data = """
1345 img_data = """
1342 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1346 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1343 width="{size}" height="{size}"
1347 width="{size}" height="{size}"
1344 style="width: 100%; height: 100%; background-color: {background}"
1348 style="width: 100%; height: 100%; background-color: {background}"
1345 viewBox="0 0 {size} {size}">
1349 viewBox="0 0 {size} {size}">
1346 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1350 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1347 pointer-events="auto" fill="{text_color}"
1351 pointer-events="auto" fill="{text_color}"
1348 font-family="{font_family}"
1352 font-family="{font_family}"
1349 style="font-weight: 400; font-size: {f_size}px;">{text}
1353 style="font-weight: 400; font-size: {f_size}px;">{text}
1350 </text>
1354 </text>
1351 </svg>""".format(
1355 </svg>""".format(
1352 size=self.size,
1356 size=self.size,
1353 f_size=self.size/2.05, # scale the text inside the box nicely
1357 f_size=self.size/2.05, # scale the text inside the box nicely
1354 background=self.background,
1358 background=self.background,
1355 text_color=self.text_color,
1359 text_color=self.text_color,
1356 text=initials.upper(),
1360 text=initials.upper(),
1357 font_family=font_family)
1361 font_family=font_family)
1358
1362
1359 return img_data
1363 return img_data
1360
1364
1361 def generate_svg(self, svg_type=None):
1365 def generate_svg(self, svg_type=None):
1362 img_data = self.get_img_data(svg_type)
1366 img_data = self.get_img_data(svg_type)
1363 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1367 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1364
1368
1365
1369
1366 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1370 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1367
1371
1368 svg_type = None
1372 svg_type = None
1369 if email_address == User.DEFAULT_USER_EMAIL:
1373 if email_address == User.DEFAULT_USER_EMAIL:
1370 svg_type = 'default_user'
1374 svg_type = 'default_user'
1371
1375
1372 klass = InitialsGravatar(email_address, first_name, last_name, size)
1376 klass = InitialsGravatar(email_address, first_name, last_name, size)
1373
1377
1374 if store_on_disk:
1378 if store_on_disk:
1375 from rhodecode.apps.file_store import utils as store_utils
1379 from rhodecode.apps.file_store import utils as store_utils
1376 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1380 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1377 FileOverSizeException
1381 FileOverSizeException
1378 from rhodecode.model.db import Session
1382 from rhodecode.model.db import Session
1379
1383
1380 image_key = md5_safe(email_address.lower()
1384 image_key = md5_safe(email_address.lower()
1381 + first_name.lower() + last_name.lower())
1385 + first_name.lower() + last_name.lower())
1382
1386
1383 storage = store_utils.get_file_storage(request.registry.settings)
1387 storage = store_utils.get_file_storage(request.registry.settings)
1384 filename = '{}.svg'.format(image_key)
1388 filename = '{}.svg'.format(image_key)
1385 subdir = 'gravatars'
1389 subdir = 'gravatars'
1386 # since final name has a counter, we apply the 0
1390 # since final name has a counter, we apply the 0
1387 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1391 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1388 store_uid = os.path.join(subdir, uid)
1392 store_uid = os.path.join(subdir, uid)
1389
1393
1390 db_entry = FileStore.get_by_store_uid(store_uid)
1394 db_entry = FileStore.get_by_store_uid(store_uid)
1391 if db_entry:
1395 if db_entry:
1392 return request.route_path('download_file', fid=store_uid)
1396 return request.route_path('download_file', fid=store_uid)
1393
1397
1394 img_data = klass.get_img_data(svg_type=svg_type)
1398 img_data = klass.get_img_data(svg_type=svg_type)
1395 img_file = store_utils.bytes_to_file_obj(img_data)
1399 img_file = store_utils.bytes_to_file_obj(img_data)
1396
1400
1397 try:
1401 try:
1398 store_uid, metadata = storage.save_file(
1402 store_uid, metadata = storage.save_file(
1399 img_file, filename, directory=subdir,
1403 img_file, filename, directory=subdir,
1400 extensions=['.svg'], randomized_name=False)
1404 extensions=['.svg'], randomized_name=False)
1401 except (FileNotAllowedException, FileOverSizeException):
1405 except (FileNotAllowedException, FileOverSizeException):
1402 raise
1406 raise
1403
1407
1404 try:
1408 try:
1405 entry = FileStore.create(
1409 entry = FileStore.create(
1406 file_uid=store_uid, filename=metadata["filename"],
1410 file_uid=store_uid, filename=metadata["filename"],
1407 file_hash=metadata["sha256"], file_size=metadata["size"],
1411 file_hash=metadata["sha256"], file_size=metadata["size"],
1408 file_display_name=filename,
1412 file_display_name=filename,
1409 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1413 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1410 hidden=True, check_acl=False, user_id=1
1414 hidden=True, check_acl=False, user_id=1
1411 )
1415 )
1412 Session().add(entry)
1416 Session().add(entry)
1413 Session().commit()
1417 Session().commit()
1414 log.debug('Stored upload in DB as %s', entry)
1418 log.debug('Stored upload in DB as %s', entry)
1415 except Exception:
1419 except Exception:
1416 raise
1420 raise
1417
1421
1418 return request.route_path('download_file', fid=store_uid)
1422 return request.route_path('download_file', fid=store_uid)
1419
1423
1420 else:
1424 else:
1421 return klass.generate_svg(svg_type=svg_type)
1425 return klass.generate_svg(svg_type=svg_type)
1422
1426
1423
1427
1424 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1428 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1425 return safe_str(gravatar_url_tmpl)\
1429 return safe_str(gravatar_url_tmpl)\
1426 .replace('{email}', email_address) \
1430 .replace('{email}', email_address) \
1427 .replace('{md5email}', md5_safe(email_address.lower())) \
1431 .replace('{md5email}', md5_safe(email_address.lower())) \
1428 .replace('{netloc}', request.host) \
1432 .replace('{netloc}', request.host) \
1429 .replace('{scheme}', request.scheme) \
1433 .replace('{scheme}', request.scheme) \
1430 .replace('{size}', safe_str(size))
1434 .replace('{size}', safe_str(size))
1431
1435
1432
1436
1433 def gravatar_url(email_address, size=30, request=None):
1437 def gravatar_url(email_address, size=30, request=None):
1434 request = request or get_current_request()
1438 request = request or get_current_request()
1435 _use_gravatar = request.call_context.visual.use_gravatar
1439 _use_gravatar = request.call_context.visual.use_gravatar
1436
1440
1437 email_address = email_address or User.DEFAULT_USER_EMAIL
1441 email_address = email_address or User.DEFAULT_USER_EMAIL
1438 if isinstance(email_address, unicode):
1442 if isinstance(email_address, unicode):
1439 # hashlib crashes on unicode items
1443 # hashlib crashes on unicode items
1440 email_address = safe_str(email_address)
1444 email_address = safe_str(email_address)
1441
1445
1442 # empty email or default user
1446 # empty email or default user
1443 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1447 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1444 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1448 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1445
1449
1446 if _use_gravatar:
1450 if _use_gravatar:
1447 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1451 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1448 or User.DEFAULT_GRAVATAR_URL
1452 or User.DEFAULT_GRAVATAR_URL
1449 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1453 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1450
1454
1451 else:
1455 else:
1452 return initials_gravatar(request, email_address, '', '', size=size)
1456 return initials_gravatar(request, email_address, '', '', size=size)
1453
1457
1454
1458
1455 def breadcrumb_repo_link(repo):
1459 def breadcrumb_repo_link(repo):
1456 """
1460 """
1457 Makes a breadcrumbs path link to repo
1461 Makes a breadcrumbs path link to repo
1458
1462
1459 ex::
1463 ex::
1460 group >> subgroup >> repo
1464 group >> subgroup >> repo
1461
1465
1462 :param repo: a Repository instance
1466 :param repo: a Repository instance
1463 """
1467 """
1464
1468
1465 path = [
1469 path = [
1466 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1470 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1467 title='last change:{}'.format(format_date(group.last_commit_change)))
1471 title='last change:{}'.format(format_date(group.last_commit_change)))
1468 for group in repo.groups_with_parents
1472 for group in repo.groups_with_parents
1469 ] + [
1473 ] + [
1470 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1474 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1471 title='last change:{}'.format(format_date(repo.last_commit_change)))
1475 title='last change:{}'.format(format_date(repo.last_commit_change)))
1472 ]
1476 ]
1473
1477
1474 return literal(' &raquo; '.join(path))
1478 return literal(' &raquo; '.join(path))
1475
1479
1476
1480
1477 def breadcrumb_repo_group_link(repo_group):
1481 def breadcrumb_repo_group_link(repo_group):
1478 """
1482 """
1479 Makes a breadcrumbs path link to repo
1483 Makes a breadcrumbs path link to repo
1480
1484
1481 ex::
1485 ex::
1482 group >> subgroup
1486 group >> subgroup
1483
1487
1484 :param repo_group: a Repository Group instance
1488 :param repo_group: a Repository Group instance
1485 """
1489 """
1486
1490
1487 path = [
1491 path = [
1488 link_to(group.name,
1492 link_to(group.name,
1489 route_path('repo_group_home', repo_group_name=group.group_name),
1493 route_path('repo_group_home', repo_group_name=group.group_name),
1490 title='last change:{}'.format(format_date(group.last_commit_change)))
1494 title='last change:{}'.format(format_date(group.last_commit_change)))
1491 for group in repo_group.parents
1495 for group in repo_group.parents
1492 ] + [
1496 ] + [
1493 link_to(repo_group.name,
1497 link_to(repo_group.name,
1494 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1498 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1495 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1499 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1496 ]
1500 ]
1497
1501
1498 return literal(' &raquo; '.join(path))
1502 return literal(' &raquo; '.join(path))
1499
1503
1500
1504
1501 def format_byte_size_binary(file_size):
1505 def format_byte_size_binary(file_size):
1502 """
1506 """
1503 Formats file/folder sizes to standard.
1507 Formats file/folder sizes to standard.
1504 """
1508 """
1505 if file_size is None:
1509 if file_size is None:
1506 file_size = 0
1510 file_size = 0
1507
1511
1508 formatted_size = format_byte_size(file_size, binary=True)
1512 formatted_size = format_byte_size(file_size, binary=True)
1509 return formatted_size
1513 return formatted_size
1510
1514
1511
1515
1512 def urlify_text(text_, safe=True, **href_attrs):
1516 def urlify_text(text_, safe=True, **href_attrs):
1513 """
1517 """
1514 Extract urls from text and make html links out of them
1518 Extract urls from text and make html links out of them
1515 """
1519 """
1516
1520
1517 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1521 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1518 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1522 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1519
1523
1520 def url_func(match_obj):
1524 def url_func(match_obj):
1521 url_full = match_obj.groups()[0]
1525 url_full = match_obj.groups()[0]
1522 a_options = dict(href_attrs)
1526 a_options = dict(href_attrs)
1523 a_options['href'] = url_full
1527 a_options['href'] = url_full
1524 a_text = url_full
1528 a_text = url_full
1525 return HTML.tag("a", a_text, **a_options)
1529 return HTML.tag("a", a_text, **a_options)
1526
1530
1527 _new_text = url_pat.sub(url_func, text_)
1531 _new_text = url_pat.sub(url_func, text_)
1528
1532
1529 if safe:
1533 if safe:
1530 return literal(_new_text)
1534 return literal(_new_text)
1531 return _new_text
1535 return _new_text
1532
1536
1533
1537
1534 def urlify_commits(text_, repo_name):
1538 def urlify_commits(text_, repo_name):
1535 """
1539 """
1536 Extract commit ids from text and make link from them
1540 Extract commit ids from text and make link from them
1537
1541
1538 :param text_:
1542 :param text_:
1539 :param repo_name: repo name to build the URL with
1543 :param repo_name: repo name to build the URL with
1540 """
1544 """
1541
1545
1542 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1546 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1543
1547
1544 def url_func(match_obj):
1548 def url_func(match_obj):
1545 commit_id = match_obj.groups()[1]
1549 commit_id = match_obj.groups()[1]
1546 pref = match_obj.groups()[0]
1550 pref = match_obj.groups()[0]
1547 suf = match_obj.groups()[2]
1551 suf = match_obj.groups()[2]
1548
1552
1549 tmpl = (
1553 tmpl = (
1550 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1554 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1551 '%(commit_id)s</a>%(suf)s'
1555 '%(commit_id)s</a>%(suf)s'
1552 )
1556 )
1553 return tmpl % {
1557 return tmpl % {
1554 'pref': pref,
1558 'pref': pref,
1555 'cls': 'revision-link',
1559 'cls': 'revision-link',
1556 'url': route_url(
1560 'url': route_url(
1557 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1561 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1558 'commit_id': commit_id,
1562 'commit_id': commit_id,
1559 'suf': suf,
1563 'suf': suf,
1560 'hovercard_alt': 'Commit: {}'.format(commit_id),
1564 'hovercard_alt': 'Commit: {}'.format(commit_id),
1561 'hovercard_url': route_url(
1565 'hovercard_url': route_url(
1562 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1566 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1563 }
1567 }
1564
1568
1565 new_text = url_pat.sub(url_func, text_)
1569 new_text = url_pat.sub(url_func, text_)
1566
1570
1567 return new_text
1571 return new_text
1568
1572
1569
1573
1570 def _process_url_func(match_obj, repo_name, uid, entry,
1574 def _process_url_func(match_obj, repo_name, uid, entry,
1571 return_raw_data=False, link_format='html'):
1575 return_raw_data=False, link_format='html'):
1572 pref = ''
1576 pref = ''
1573 if match_obj.group().startswith(' '):
1577 if match_obj.group().startswith(' '):
1574 pref = ' '
1578 pref = ' '
1575
1579
1576 issue_id = ''.join(match_obj.groups())
1580 issue_id = ''.join(match_obj.groups())
1577
1581
1578 if link_format == 'html':
1582 if link_format == 'html':
1579 tmpl = (
1583 tmpl = (
1580 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1584 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1581 '%(issue-prefix)s%(id-repr)s'
1585 '%(issue-prefix)s%(id-repr)s'
1582 '</a>')
1586 '</a>')
1583 elif link_format == 'html+hovercard':
1587 elif link_format == 'html+hovercard':
1584 tmpl = (
1588 tmpl = (
1585 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1589 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1586 '%(issue-prefix)s%(id-repr)s'
1590 '%(issue-prefix)s%(id-repr)s'
1587 '</a>')
1591 '</a>')
1588 elif link_format in ['rst', 'rst+hovercard']:
1592 elif link_format in ['rst', 'rst+hovercard']:
1589 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1593 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1590 elif link_format in ['markdown', 'markdown+hovercard']:
1594 elif link_format in ['markdown', 'markdown+hovercard']:
1591 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1595 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1592 else:
1596 else:
1593 raise ValueError('Bad link_format:{}'.format(link_format))
1597 raise ValueError('Bad link_format:{}'.format(link_format))
1594
1598
1595 (repo_name_cleaned,
1599 (repo_name_cleaned,
1596 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1600 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1597
1601
1598 # variables replacement
1602 # variables replacement
1599 named_vars = {
1603 named_vars = {
1600 'id': issue_id,
1604 'id': issue_id,
1601 'repo': repo_name,
1605 'repo': repo_name,
1602 'repo_name': repo_name_cleaned,
1606 'repo_name': repo_name_cleaned,
1603 'group_name': parent_group_name,
1607 'group_name': parent_group_name,
1604 # set dummy keys so we always have them
1608 # set dummy keys so we always have them
1605 'hostname': '',
1609 'hostname': '',
1606 'netloc': '',
1610 'netloc': '',
1607 'scheme': ''
1611 'scheme': ''
1608 }
1612 }
1609
1613
1610 request = get_current_request()
1614 request = get_current_request()
1611 if request:
1615 if request:
1612 # exposes, hostname, netloc, scheme
1616 # exposes, hostname, netloc, scheme
1613 host_data = get_host_info(request)
1617 host_data = get_host_info(request)
1614 named_vars.update(host_data)
1618 named_vars.update(host_data)
1615
1619
1616 # named regex variables
1620 # named regex variables
1617 named_vars.update(match_obj.groupdict())
1621 named_vars.update(match_obj.groupdict())
1618 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1622 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1619 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1623 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1620 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1624 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1621
1625
1622 def quote_cleaner(input_str):
1626 def quote_cleaner(input_str):
1623 """Remove quotes as it's HTML"""
1627 """Remove quotes as it's HTML"""
1624 return input_str.replace('"', '')
1628 return input_str.replace('"', '')
1625
1629
1626 data = {
1630 data = {
1627 'pref': pref,
1631 'pref': pref,
1628 'cls': quote_cleaner('issue-tracker-link'),
1632 'cls': quote_cleaner('issue-tracker-link'),
1629 'url': quote_cleaner(_url),
1633 'url': quote_cleaner(_url),
1630 'id-repr': issue_id,
1634 'id-repr': issue_id,
1631 'issue-prefix': entry['pref'],
1635 'issue-prefix': entry['pref'],
1632 'serv': entry['url'],
1636 'serv': entry['url'],
1633 'title': bleach.clean(desc, strip=True),
1637 'title': bleach.clean(desc, strip=True),
1634 'hovercard_url': hovercard_url
1638 'hovercard_url': hovercard_url
1635 }
1639 }
1636
1640
1637 if return_raw_data:
1641 if return_raw_data:
1638 return {
1642 return {
1639 'id': issue_id,
1643 'id': issue_id,
1640 'url': _url
1644 'url': _url
1641 }
1645 }
1642 return tmpl % data
1646 return tmpl % data
1643
1647
1644
1648
1645 def get_active_pattern_entries(repo_name):
1649 def get_active_pattern_entries(repo_name):
1646 repo = None
1650 repo = None
1647 if repo_name:
1651 if repo_name:
1648 # Retrieving repo_name to avoid invalid repo_name to explode on
1652 # Retrieving repo_name to avoid invalid repo_name to explode on
1649 # IssueTrackerSettingsModel but still passing invalid name further down
1653 # IssueTrackerSettingsModel but still passing invalid name further down
1650 repo = Repository.get_by_repo_name(repo_name, cache=True)
1654 repo = Repository.get_by_repo_name(repo_name, cache=True)
1651
1655
1652 settings_model = IssueTrackerSettingsModel(repo=repo)
1656 settings_model = IssueTrackerSettingsModel(repo=repo)
1653 active_entries = settings_model.get_settings(cache=True)
1657 active_entries = settings_model.get_settings(cache=True)
1654 return active_entries
1658 return active_entries
1655
1659
1656
1660
1657 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1661 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1658
1662
1659 allowed_link_formats = [
1663 allowed_link_formats = [
1660 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1664 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1661
1665
1662
1666
1663 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1667 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1664
1668
1665 if link_format not in allowed_link_formats:
1669 if link_format not in allowed_link_formats:
1666 raise ValueError('Link format can be only one of:{} got {}'.format(
1670 raise ValueError('Link format can be only one of:{} got {}'.format(
1667 allowed_link_formats, link_format))
1671 allowed_link_formats, link_format))
1668
1672
1669 if active_entries is None:
1673 if active_entries is None:
1670 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1674 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1671 active_entries = get_active_pattern_entries(repo_name)
1675 active_entries = get_active_pattern_entries(repo_name)
1672
1676
1673 issues_data = []
1677 issues_data = []
1674 errors = []
1678 errors = []
1675 new_text = text_string
1679 new_text = text_string
1676
1680
1677 log.debug('Got %s entries to process', len(active_entries))
1681 log.debug('Got %s entries to process', len(active_entries))
1678 for uid, entry in active_entries.items():
1682 for uid, entry in active_entries.items():
1679 log.debug('found issue tracker entry with uid %s', uid)
1683 log.debug('found issue tracker entry with uid %s', uid)
1680
1684
1681 if not (entry['pat'] and entry['url']):
1685 if not (entry['pat'] and entry['url']):
1682 log.debug('skipping due to missing data')
1686 log.debug('skipping due to missing data')
1683 continue
1687 continue
1684
1688
1685 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1689 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1686 uid, entry['pat'], entry['url'], entry['pref'])
1690 uid, entry['pat'], entry['url'], entry['pref'])
1687
1691
1688 if entry.get('pat_compiled'):
1692 if entry.get('pat_compiled'):
1689 pattern = entry['pat_compiled']
1693 pattern = entry['pat_compiled']
1690 else:
1694 else:
1691 try:
1695 try:
1692 pattern = regex.compile(r'%s' % entry['pat'])
1696 pattern = regex.compile(r'%s' % entry['pat'])
1693 except regex.error as e:
1697 except regex.error as e:
1694 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1698 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1695 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1699 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1696 errors.append(regex_err)
1700 errors.append(regex_err)
1697 continue
1701 continue
1698
1702
1699 data_func = partial(
1703 data_func = partial(
1700 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1704 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1701 return_raw_data=True)
1705 return_raw_data=True)
1702
1706
1703 for match_obj in pattern.finditer(text_string):
1707 for match_obj in pattern.finditer(text_string):
1704 issues_data.append(data_func(match_obj))
1708 issues_data.append(data_func(match_obj))
1705
1709
1706 url_func = partial(
1710 url_func = partial(
1707 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1711 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1708 link_format=link_format)
1712 link_format=link_format)
1709
1713
1710 new_text = pattern.sub(url_func, new_text)
1714 new_text = pattern.sub(url_func, new_text)
1711 log.debug('processed prefix:uid `%s`', uid)
1715 log.debug('processed prefix:uid `%s`', uid)
1712
1716
1713 # finally use global replace, eg !123 -> pr-link, those will not catch
1717 # finally use global replace, eg !123 -> pr-link, those will not catch
1714 # if already similar pattern exists
1718 # if already similar pattern exists
1715 server_url = '${scheme}://${netloc}'
1719 server_url = '${scheme}://${netloc}'
1716 pr_entry = {
1720 pr_entry = {
1717 'pref': '!',
1721 'pref': '!',
1718 'url': server_url + '/_admin/pull-requests/${id}',
1722 'url': server_url + '/_admin/pull-requests/${id}',
1719 'desc': 'Pull Request !${id}',
1723 'desc': 'Pull Request !${id}',
1720 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1724 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1721 }
1725 }
1722 pr_url_func = partial(
1726 pr_url_func = partial(
1723 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1727 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1724 link_format=link_format+'+hovercard')
1728 link_format=link_format+'+hovercard')
1725 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1729 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1726 log.debug('processed !pr pattern')
1730 log.debug('processed !pr pattern')
1727
1731
1728 return new_text, issues_data, errors
1732 return new_text, issues_data, errors
1729
1733
1730
1734
1731 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1735 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1732 issues_container=None, error_container=None):
1736 issues_container=None, error_container=None):
1733 """
1737 """
1734 Parses given text message and makes proper links.
1738 Parses given text message and makes proper links.
1735 issues are linked to given issue-server, and rest is a commit link
1739 issues are linked to given issue-server, and rest is a commit link
1736 """
1740 """
1737
1741
1738 def escaper(_text):
1742 def escaper(_text):
1739 return _text.replace('<', '&lt;').replace('>', '&gt;')
1743 return _text.replace('<', '&lt;').replace('>', '&gt;')
1740
1744
1741 new_text = escaper(commit_text)
1745 new_text = escaper(commit_text)
1742
1746
1743 # extract http/https links and make them real urls
1747 # extract http/https links and make them real urls
1744 new_text = urlify_text(new_text, safe=False)
1748 new_text = urlify_text(new_text, safe=False)
1745
1749
1746 # urlify commits - extract commit ids and make link out of them, if we have
1750 # urlify commits - extract commit ids and make link out of them, if we have
1747 # the scope of repository present.
1751 # the scope of repository present.
1748 if repository:
1752 if repository:
1749 new_text = urlify_commits(new_text, repository)
1753 new_text = urlify_commits(new_text, repository)
1750
1754
1751 # process issue tracker patterns
1755 # process issue tracker patterns
1752 new_text, issues, errors = process_patterns(
1756 new_text, issues, errors = process_patterns(
1753 new_text, repository or '', active_entries=active_pattern_entries)
1757 new_text, repository or '', active_entries=active_pattern_entries)
1754
1758
1755 if issues_container is not None:
1759 if issues_container is not None:
1756 issues_container.extend(issues)
1760 issues_container.extend(issues)
1757
1761
1758 if error_container is not None:
1762 if error_container is not None:
1759 error_container.extend(errors)
1763 error_container.extend(errors)
1760
1764
1761 return literal(new_text)
1765 return literal(new_text)
1762
1766
1763
1767
1764 def render_binary(repo_name, file_obj):
1768 def render_binary(repo_name, file_obj):
1765 """
1769 """
1766 Choose how to render a binary file
1770 Choose how to render a binary file
1767 """
1771 """
1768
1772
1769 # unicode
1773 # unicode
1770 filename = file_obj.name
1774 filename = file_obj.name
1771
1775
1772 # images
1776 # images
1773 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1777 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1774 if fnmatch.fnmatch(filename, pat=ext):
1778 if fnmatch.fnmatch(filename, pat=ext):
1775 src = route_path(
1779 src = route_path(
1776 'repo_file_raw', repo_name=repo_name,
1780 'repo_file_raw', repo_name=repo_name,
1777 commit_id=file_obj.commit.raw_id,
1781 commit_id=file_obj.commit.raw_id,
1778 f_path=file_obj.path)
1782 f_path=file_obj.path)
1779
1783
1780 return literal(
1784 return literal(
1781 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1785 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1782
1786
1783
1787
1784 def renderer_from_filename(filename, exclude=None):
1788 def renderer_from_filename(filename, exclude=None):
1785 """
1789 """
1786 choose a renderer based on filename, this works only for text based files
1790 choose a renderer based on filename, this works only for text based files
1787 """
1791 """
1788
1792
1789 # ipython
1793 # ipython
1790 for ext in ['*.ipynb']:
1794 for ext in ['*.ipynb']:
1791 if fnmatch.fnmatch(filename, pat=ext):
1795 if fnmatch.fnmatch(filename, pat=ext):
1792 return 'jupyter'
1796 return 'jupyter'
1793
1797
1794 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1798 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1795 if is_markup:
1799 if is_markup:
1796 return is_markup
1800 return is_markup
1797 return None
1801 return None
1798
1802
1799
1803
1800 def render(source, renderer='rst', mentions=False, relative_urls=None,
1804 def render(source, renderer='rst', mentions=False, relative_urls=None,
1801 repo_name=None, active_pattern_entries=None, issues_container=None):
1805 repo_name=None, active_pattern_entries=None, issues_container=None):
1802
1806
1803 def maybe_convert_relative_links(html_source):
1807 def maybe_convert_relative_links(html_source):
1804 if relative_urls:
1808 if relative_urls:
1805 return relative_links(html_source, relative_urls)
1809 return relative_links(html_source, relative_urls)
1806 return html_source
1810 return html_source
1807
1811
1808 if renderer == 'plain':
1812 if renderer == 'plain':
1809 return literal(
1813 return literal(
1810 MarkupRenderer.plain(source, leading_newline=False))
1814 MarkupRenderer.plain(source, leading_newline=False))
1811
1815
1812 elif renderer == 'rst':
1816 elif renderer == 'rst':
1813 if repo_name:
1817 if repo_name:
1814 # process patterns on comments if we pass in repo name
1818 # process patterns on comments if we pass in repo name
1815 source, issues, errors = process_patterns(
1819 source, issues, errors = process_patterns(
1816 source, repo_name, link_format='rst',
1820 source, repo_name, link_format='rst',
1817 active_entries=active_pattern_entries)
1821 active_entries=active_pattern_entries)
1818 if issues_container is not None:
1822 if issues_container is not None:
1819 issues_container.extend(issues)
1823 issues_container.extend(issues)
1820
1824
1821 return literal(
1825 return literal(
1822 '<div class="rst-block">%s</div>' %
1826 '<div class="rst-block">%s</div>' %
1823 maybe_convert_relative_links(
1827 maybe_convert_relative_links(
1824 MarkupRenderer.rst(source, mentions=mentions)))
1828 MarkupRenderer.rst(source, mentions=mentions)))
1825
1829
1826 elif renderer == 'markdown':
1830 elif renderer == 'markdown':
1827 if repo_name:
1831 if repo_name:
1828 # process patterns on comments if we pass in repo name
1832 # process patterns on comments if we pass in repo name
1829 source, issues, errors = process_patterns(
1833 source, issues, errors = process_patterns(
1830 source, repo_name, link_format='markdown',
1834 source, repo_name, link_format='markdown',
1831 active_entries=active_pattern_entries)
1835 active_entries=active_pattern_entries)
1832 if issues_container is not None:
1836 if issues_container is not None:
1833 issues_container.extend(issues)
1837 issues_container.extend(issues)
1834
1838
1835 return literal(
1839 return literal(
1836 '<div class="markdown-block">%s</div>' %
1840 '<div class="markdown-block">%s</div>' %
1837 maybe_convert_relative_links(
1841 maybe_convert_relative_links(
1838 MarkupRenderer.markdown(source, flavored=True,
1842 MarkupRenderer.markdown(source, flavored=True,
1839 mentions=mentions)))
1843 mentions=mentions)))
1840
1844
1841 elif renderer == 'jupyter':
1845 elif renderer == 'jupyter':
1842 return literal(
1846 return literal(
1843 '<div class="ipynb">%s</div>' %
1847 '<div class="ipynb">%s</div>' %
1844 maybe_convert_relative_links(
1848 maybe_convert_relative_links(
1845 MarkupRenderer.jupyter(source)))
1849 MarkupRenderer.jupyter(source)))
1846
1850
1847 # None means just show the file-source
1851 # None means just show the file-source
1848 return None
1852 return None
1849
1853
1850
1854
1851 def commit_status(repo, commit_id):
1855 def commit_status(repo, commit_id):
1852 return ChangesetStatusModel().get_status(repo, commit_id)
1856 return ChangesetStatusModel().get_status(repo, commit_id)
1853
1857
1854
1858
1855 def commit_status_lbl(commit_status):
1859 def commit_status_lbl(commit_status):
1856 return dict(ChangesetStatus.STATUSES).get(commit_status)
1860 return dict(ChangesetStatus.STATUSES).get(commit_status)
1857
1861
1858
1862
1859 def commit_time(repo_name, commit_id):
1863 def commit_time(repo_name, commit_id):
1860 repo = Repository.get_by_repo_name(repo_name)
1864 repo = Repository.get_by_repo_name(repo_name)
1861 commit = repo.get_commit(commit_id=commit_id)
1865 commit = repo.get_commit(commit_id=commit_id)
1862 return commit.date
1866 return commit.date
1863
1867
1864
1868
1865 def get_permission_name(key):
1869 def get_permission_name(key):
1866 return dict(Permission.PERMS).get(key)
1870 return dict(Permission.PERMS).get(key)
1867
1871
1868
1872
1869 def journal_filter_help(request):
1873 def journal_filter_help(request):
1870 _ = request.translate
1874 _ = request.translate
1871 from rhodecode.lib.audit_logger import ACTIONS
1875 from rhodecode.lib.audit_logger import ACTIONS
1872 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1876 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1873
1877
1874 return _(
1878 return _(
1875 'Example filter terms:\n' +
1879 'Example filter terms:\n' +
1876 ' repository:vcs\n' +
1880 ' repository:vcs\n' +
1877 ' username:marcin\n' +
1881 ' username:marcin\n' +
1878 ' username:(NOT marcin)\n' +
1882 ' username:(NOT marcin)\n' +
1879 ' action:*push*\n' +
1883 ' action:*push*\n' +
1880 ' ip:127.0.0.1\n' +
1884 ' ip:127.0.0.1\n' +
1881 ' date:20120101\n' +
1885 ' date:20120101\n' +
1882 ' date:[20120101100000 TO 20120102]\n' +
1886 ' date:[20120101100000 TO 20120102]\n' +
1883 '\n' +
1887 '\n' +
1884 'Actions: {actions}\n' +
1888 'Actions: {actions}\n' +
1885 '\n' +
1889 '\n' +
1886 'Generate wildcards using \'*\' character:\n' +
1890 'Generate wildcards using \'*\' character:\n' +
1887 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1891 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1888 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1892 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1889 '\n' +
1893 '\n' +
1890 'Optional AND / OR operators in queries\n' +
1894 'Optional AND / OR operators in queries\n' +
1891 ' "repository:vcs OR repository:test"\n' +
1895 ' "repository:vcs OR repository:test"\n' +
1892 ' "username:test AND repository:test*"\n'
1896 ' "username:test AND repository:test*"\n'
1893 ).format(actions=actions)
1897 ).format(actions=actions)
1894
1898
1895
1899
1896 def not_mapped_error(repo_name):
1900 def not_mapped_error(repo_name):
1897 from rhodecode.translation import _
1901 from rhodecode.translation import _
1898 flash(_('%s repository is not mapped to db perhaps'
1902 flash(_('%s repository is not mapped to db perhaps'
1899 ' it was created or renamed from the filesystem'
1903 ' it was created or renamed from the filesystem'
1900 ' please run the application again'
1904 ' please run the application again'
1901 ' in order to rescan repositories') % repo_name, category='error')
1905 ' in order to rescan repositories') % repo_name, category='error')
1902
1906
1903
1907
1904 def ip_range(ip_addr):
1908 def ip_range(ip_addr):
1905 from rhodecode.model.db import UserIpMap
1909 from rhodecode.model.db import UserIpMap
1906 s, e = UserIpMap._get_ip_range(ip_addr)
1910 s, e = UserIpMap._get_ip_range(ip_addr)
1907 return '%s - %s' % (s, e)
1911 return '%s - %s' % (s, e)
1908
1912
1909
1913
1910 def form(url, method='post', needs_csrf_token=True, **attrs):
1914 def form(url, method='post', needs_csrf_token=True, **attrs):
1911 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1915 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1912 if method.lower() != 'get' and needs_csrf_token:
1916 if method.lower() != 'get' and needs_csrf_token:
1913 raise Exception(
1917 raise Exception(
1914 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1918 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1915 'CSRF token. If the endpoint does not require such token you can ' +
1919 'CSRF token. If the endpoint does not require such token you can ' +
1916 'explicitly set the parameter needs_csrf_token to false.')
1920 'explicitly set the parameter needs_csrf_token to false.')
1917
1921
1918 return insecure_form(url, method=method, **attrs)
1922 return insecure_form(url, method=method, **attrs)
1919
1923
1920
1924
1921 def secure_form(form_url, method="POST", multipart=False, **attrs):
1925 def secure_form(form_url, method="POST", multipart=False, **attrs):
1922 """Start a form tag that points the action to an url. This
1926 """Start a form tag that points the action to an url. This
1923 form tag will also include the hidden field containing
1927 form tag will also include the hidden field containing
1924 the auth token.
1928 the auth token.
1925
1929
1926 The url options should be given either as a string, or as a
1930 The url options should be given either as a string, or as a
1927 ``url()`` function. The method for the form defaults to POST.
1931 ``url()`` function. The method for the form defaults to POST.
1928
1932
1929 Options:
1933 Options:
1930
1934
1931 ``multipart``
1935 ``multipart``
1932 If set to True, the enctype is set to "multipart/form-data".
1936 If set to True, the enctype is set to "multipart/form-data".
1933 ``method``
1937 ``method``
1934 The method to use when submitting the form, usually either
1938 The method to use when submitting the form, usually either
1935 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1939 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1936 hidden input with name _method is added to simulate the verb
1940 hidden input with name _method is added to simulate the verb
1937 over POST.
1941 over POST.
1938
1942
1939 """
1943 """
1940
1944
1941 if 'request' in attrs:
1945 if 'request' in attrs:
1942 session = attrs['request'].session
1946 session = attrs['request'].session
1943 del attrs['request']
1947 del attrs['request']
1944 else:
1948 else:
1945 raise ValueError(
1949 raise ValueError(
1946 'Calling this form requires request= to be passed as argument')
1950 'Calling this form requires request= to be passed as argument')
1947
1951
1948 _form = insecure_form(form_url, method, multipart, **attrs)
1952 _form = insecure_form(form_url, method, multipart, **attrs)
1949 token = literal(
1953 token = literal(
1950 '<input type="hidden" name="{}" value="{}">'.format(
1954 '<input type="hidden" name="{}" value="{}">'.format(
1951 csrf_token_key, get_csrf_token(session)))
1955 csrf_token_key, get_csrf_token(session)))
1952
1956
1953 return literal("%s\n%s" % (_form, token))
1957 return literal("%s\n%s" % (_form, token))
1954
1958
1955
1959
1956 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1960 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1957 select_html = select(name, selected, options, **attrs)
1961 select_html = select(name, selected, options, **attrs)
1958
1962
1959 select2 = """
1963 select2 = """
1960 <script>
1964 <script>
1961 $(document).ready(function() {
1965 $(document).ready(function() {
1962 $('#%s').select2({
1966 $('#%s').select2({
1963 containerCssClass: 'drop-menu %s',
1967 containerCssClass: 'drop-menu %s',
1964 dropdownCssClass: 'drop-menu-dropdown',
1968 dropdownCssClass: 'drop-menu-dropdown',
1965 dropdownAutoWidth: true%s
1969 dropdownAutoWidth: true%s
1966 });
1970 });
1967 });
1971 });
1968 </script>
1972 </script>
1969 """
1973 """
1970
1974
1971 filter_option = """,
1975 filter_option = """,
1972 minimumResultsForSearch: -1
1976 minimumResultsForSearch: -1
1973 """
1977 """
1974 input_id = attrs.get('id') or name
1978 input_id = attrs.get('id') or name
1975 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1979 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1976 filter_enabled = "" if enable_filter else filter_option
1980 filter_enabled = "" if enable_filter else filter_option
1977 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1981 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1978
1982
1979 return literal(select_html+select_script)
1983 return literal(select_html+select_script)
1980
1984
1981
1985
1982 def get_visual_attr(tmpl_context_var, attr_name):
1986 def get_visual_attr(tmpl_context_var, attr_name):
1983 """
1987 """
1984 A safe way to get a variable from visual variable of template context
1988 A safe way to get a variable from visual variable of template context
1985
1989
1986 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1990 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1987 :param attr_name: name of the attribute we fetch from the c.visual
1991 :param attr_name: name of the attribute we fetch from the c.visual
1988 """
1992 """
1989 visual = getattr(tmpl_context_var, 'visual', None)
1993 visual = getattr(tmpl_context_var, 'visual', None)
1990 if not visual:
1994 if not visual:
1991 return
1995 return
1992 else:
1996 else:
1993 return getattr(visual, attr_name, None)
1997 return getattr(visual, attr_name, None)
1994
1998
1995
1999
1996 def get_last_path_part(file_node):
2000 def get_last_path_part(file_node):
1997 if not file_node.path:
2001 if not file_node.path:
1998 return u'/'
2002 return u'/'
1999
2003
2000 path = safe_unicode(file_node.path.split('/')[-1])
2004 path = safe_unicode(file_node.path.split('/')[-1])
2001 return u'../' + path
2005 return u'../' + path
2002
2006
2003
2007
2004 def route_url(*args, **kwargs):
2008 def route_url(*args, **kwargs):
2005 """
2009 """
2006 Wrapper around pyramids `route_url` (fully qualified url) function.
2010 Wrapper around pyramids `route_url` (fully qualified url) function.
2007 """
2011 """
2008 req = get_current_request()
2012 req = get_current_request()
2009 return req.route_url(*args, **kwargs)
2013 return req.route_url(*args, **kwargs)
2010
2014
2011
2015
2012 def route_path(*args, **kwargs):
2016 def route_path(*args, **kwargs):
2013 """
2017 """
2014 Wrapper around pyramids `route_path` function.
2018 Wrapper around pyramids `route_path` function.
2015 """
2019 """
2016 req = get_current_request()
2020 req = get_current_request()
2017 return req.route_path(*args, **kwargs)
2021 return req.route_path(*args, **kwargs)
2018
2022
2019
2023
2020 def route_path_or_none(*args, **kwargs):
2024 def route_path_or_none(*args, **kwargs):
2021 try:
2025 try:
2022 return route_path(*args, **kwargs)
2026 return route_path(*args, **kwargs)
2023 except KeyError:
2027 except KeyError:
2024 return None
2028 return None
2025
2029
2026
2030
2027 def current_route_path(request, **kw):
2031 def current_route_path(request, **kw):
2028 new_args = request.GET.mixed()
2032 new_args = request.GET.mixed()
2029 new_args.update(kw)
2033 new_args.update(kw)
2030 return request.current_route_path(_query=new_args)
2034 return request.current_route_path(_query=new_args)
2031
2035
2032
2036
2033 def curl_api_example(method, args):
2037 def curl_api_example(method, args):
2034 args_json = json.dumps(OrderedDict([
2038 args_json = json.dumps(OrderedDict([
2035 ('id', 1),
2039 ('id', 1),
2036 ('auth_token', 'SECRET'),
2040 ('auth_token', 'SECRET'),
2037 ('method', method),
2041 ('method', method),
2038 ('args', args)
2042 ('args', args)
2039 ]))
2043 ]))
2040
2044
2041 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2045 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2042 api_url=route_url('apiv2'),
2046 api_url=route_url('apiv2'),
2043 args_json=args_json
2047 args_json=args_json
2044 )
2048 )
2045
2049
2046
2050
2047 def api_call_example(method, args):
2051 def api_call_example(method, args):
2048 """
2052 """
2049 Generates an API call example via CURL
2053 Generates an API call example via CURL
2050 """
2054 """
2051 curl_call = curl_api_example(method, args)
2055 curl_call = curl_api_example(method, args)
2052
2056
2053 return literal(
2057 return literal(
2054 curl_call +
2058 curl_call +
2055 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2059 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2056 "and needs to be of `api calls` role."
2060 "and needs to be of `api calls` role."
2057 .format(token_url=route_url('my_account_auth_tokens')))
2061 .format(token_url=route_url('my_account_auth_tokens')))
2058
2062
2059
2063
2060 def notification_description(notification, request):
2064 def notification_description(notification, request):
2061 """
2065 """
2062 Generate notification human readable description based on notification type
2066 Generate notification human readable description based on notification type
2063 """
2067 """
2064 from rhodecode.model.notification import NotificationModel
2068 from rhodecode.model.notification import NotificationModel
2065 return NotificationModel().make_description(
2069 return NotificationModel().make_description(
2066 notification, translate=request.translate)
2070 notification, translate=request.translate)
2067
2071
2068
2072
2069 def go_import_header(request, db_repo=None):
2073 def go_import_header(request, db_repo=None):
2070 """
2074 """
2071 Creates a header for go-import functionality in Go Lang
2075 Creates a header for go-import functionality in Go Lang
2072 """
2076 """
2073
2077
2074 if not db_repo:
2078 if not db_repo:
2075 return
2079 return
2076 if 'go-get' not in request.GET:
2080 if 'go-get' not in request.GET:
2077 return
2081 return
2078
2082
2079 clone_url = db_repo.clone_url()
2083 clone_url = db_repo.clone_url()
2080 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2084 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2081 # we have a repo and go-get flag,
2085 # we have a repo and go-get flag,
2082 return literal('<meta name="go-import" content="{} {} {}">'.format(
2086 return literal('<meta name="go-import" content="{} {} {}">'.format(
2083 prefix, db_repo.repo_type, clone_url))
2087 prefix, db_repo.repo_type, clone_url))
2084
2088
2085
2089
2086 def reviewer_as_json(*args, **kwargs):
2090 def reviewer_as_json(*args, **kwargs):
2087 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2091 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2088 return _reviewer_as_json(*args, **kwargs)
2092 return _reviewer_as_json(*args, **kwargs)
2089
2093
2090
2094
2091 def get_repo_view_type(request):
2095 def get_repo_view_type(request):
2092 route_name = request.matched_route.name
2096 route_name = request.matched_route.name
2093 route_to_view_type = {
2097 route_to_view_type = {
2094 'repo_changelog': 'commits',
2098 'repo_changelog': 'commits',
2095 'repo_commits': 'commits',
2099 'repo_commits': 'commits',
2096 'repo_files': 'files',
2100 'repo_files': 'files',
2097 'repo_summary': 'summary',
2101 'repo_summary': 'summary',
2098 'repo_commit': 'commit'
2102 'repo_commit': 'commit'
2099 }
2103 }
2100
2104
2101 return route_to_view_type.get(route_name)
2105 return route_to_view_type.get(route_name)
2102
2106
2103
2107
2104 def is_active(menu_entry, selected):
2108 def is_active(menu_entry, selected):
2105 """
2109 """
2106 Returns active class for selecting menus in templates
2110 Returns active class for selecting menus in templates
2107 <li class=${h.is_active('settings', current_active)}></li>
2111 <li class=${h.is_active('settings', current_active)}></li>
2108 """
2112 """
2109 if not isinstance(menu_entry, list):
2113 if not isinstance(menu_entry, list):
2110 menu_entry = [menu_entry]
2114 menu_entry = [menu_entry]
2111
2115
2112 if selected in menu_entry:
2116 if selected in menu_entry:
2113 return "active"
2117 return "active"
@@ -1,397 +1,396 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 import itertools
22 import itertools
23 import logging
23 import logging
24 import collections
24 import collections
25
25
26 from rhodecode.model import BaseModel
26 from rhodecode.model import BaseModel
27 from rhodecode.model.db import (
27 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, Session)
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 from rhodecode.lib.markup_renderer import (
30 from rhodecode.lib.markup_renderer import (
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class ChangesetStatusModel(BaseModel):
36 class ChangesetStatusModel(BaseModel):
37
37
38 cls = ChangesetStatus
38 cls = ChangesetStatus
39
39
40 def __get_changeset_status(self, changeset_status):
40 def __get_changeset_status(self, changeset_status):
41 return self._get_instance(ChangesetStatus, changeset_status)
41 return self._get_instance(ChangesetStatus, changeset_status)
42
42
43 def __get_pull_request(self, pull_request):
43 def __get_pull_request(self, pull_request):
44 return self._get_instance(PullRequest, pull_request)
44 return self._get_instance(PullRequest, pull_request)
45
45
46 def _get_status_query(self, repo, revision, pull_request,
46 def _get_status_query(self, repo, revision, pull_request,
47 with_revisions=False):
47 with_revisions=False):
48 repo = self._get_repo(repo)
48 repo = self._get_repo(repo)
49
49
50 q = ChangesetStatus.query()\
50 q = ChangesetStatus.query()\
51 .filter(ChangesetStatus.repo == repo)
51 .filter(ChangesetStatus.repo == repo)
52 if not with_revisions:
52 if not with_revisions:
53 q = q.filter(ChangesetStatus.version == 0)
53 q = q.filter(ChangesetStatus.version == 0)
54
54
55 if revision:
55 if revision:
56 q = q.filter(ChangesetStatus.revision == revision)
56 q = q.filter(ChangesetStatus.revision == revision)
57 elif pull_request:
57 elif pull_request:
58 pull_request = self.__get_pull_request(pull_request)
58 pull_request = self.__get_pull_request(pull_request)
59 # TODO: johbo: Think about the impact of this join, there must
59 # TODO: johbo: Think about the impact of this join, there must
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
61 # to the pull request. Might be that we want to do the same for
61 # to the pull request. Might be that we want to do the same for
62 # the pull_request_version_id.
62 # the pull_request_version_id.
63 q = q.join(ChangesetComment).filter(
63 q = q.join(ChangesetComment).filter(
64 ChangesetStatus.pull_request == pull_request,
64 ChangesetStatus.pull_request == pull_request,
65 ChangesetComment.pull_request_version_id == None)
65 ChangesetComment.pull_request_version_id == None)
66 else:
66 else:
67 raise Exception('Please specify revision or pull_request')
67 raise Exception('Please specify revision or pull_request')
68 q = q.order_by(ChangesetStatus.version.asc())
68 q = q.order_by(ChangesetStatus.version.asc())
69 return q
69 return q
70
70
71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
72 trim_votes=True):
72 trim_votes=True):
73 """
73 """
74 Calculate status based on given group members, and voting rule
74 Calculate status based on given group members, and voting rule
75
75
76
76
77 group1 - 4 members, 3 required for approval
77 group1 - 4 members, 3 required for approval
78 user1 - approved
78 user1 - approved
79 user2 - reject
79 user2 - reject
80 user3 - approved
80 user3 - approved
81 user4 - rejected
81 user4 - rejected
82
82
83 final_state: rejected, reasons not at least 3 votes
83 final_state: rejected, reasons not at least 3 votes
84
84
85
85
86 group1 - 4 members, 2 required for approval
86 group1 - 4 members, 2 required for approval
87 user1 - approved
87 user1 - approved
88 user2 - reject
88 user2 - reject
89 user3 - approved
89 user3 - approved
90 user4 - rejected
90 user4 - rejected
91
91
92 final_state: approved, reasons got at least 2 approvals
92 final_state: approved, reasons got at least 2 approvals
93
93
94 group1 - 4 members, ALL required for approval
94 group1 - 4 members, ALL required for approval
95 user1 - approved
95 user1 - approved
96 user2 - reject
96 user2 - reject
97 user3 - approved
97 user3 - approved
98 user4 - rejected
98 user4 - rejected
99
99
100 final_state: rejected, reasons not all approvals
100 final_state: rejected, reasons not all approvals
101
101
102
102
103 group1 - 4 members, ALL required for approval
103 group1 - 4 members, ALL required for approval
104 user1 - approved
104 user1 - approved
105 user2 - approved
105 user2 - approved
106 user3 - approved
106 user3 - approved
107 user4 - approved
107 user4 - approved
108
108
109 final_state: approved, reason all approvals received
109 final_state: approved, reason all approvals received
110
110
111 group1 - 4 members, 5 required for approval
111 group1 - 4 members, 5 required for approval
112 (approval should be shorted to number of actual members)
112 (approval should be shorted to number of actual members)
113
113
114 user1 - approved
114 user1 - approved
115 user2 - approved
115 user2 - approved
116 user3 - approved
116 user3 - approved
117 user4 - approved
117 user4 - approved
118
118
119 final_state: approved, reason all approvals received
119 final_state: approved, reason all approvals received
120
120
121 """
121 """
122 group_vote_data = {}
122 group_vote_data = {}
123 got_rule = False
123 got_rule = False
124 members = collections.OrderedDict()
124 members = collections.OrderedDict()
125 for review_obj, user, reasons, mandatory, statuses \
125 for review_obj, user, reasons, mandatory, statuses \
126 in group_statuses_by_reviewers:
126 in group_statuses_by_reviewers:
127
127
128 if not got_rule:
128 if not got_rule:
129 group_vote_data = review_obj.rule_user_group_data()
129 group_vote_data = review_obj.rule_user_group_data()
130 got_rule = bool(group_vote_data)
130 got_rule = bool(group_vote_data)
131
131
132 members[user.user_id] = statuses
132 members[user.user_id] = statuses
133
133
134 if not group_vote_data:
134 if not group_vote_data:
135 return []
135 return []
136
136
137 required_votes = group_vote_data['vote_rule']
137 required_votes = group_vote_data['vote_rule']
138 if required_votes == -1:
138 if required_votes == -1:
139 # -1 means all required, so we replace it with how many people
139 # -1 means all required, so we replace it with how many people
140 # are in the members
140 # are in the members
141 required_votes = len(members)
141 required_votes = len(members)
142
142
143 if trim_votes and required_votes > len(members):
143 if trim_votes and required_votes > len(members):
144 # we require more votes than we have members in the group
144 # 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
145 # in this case we trim the required votes to the number of members
146 required_votes = len(members)
146 required_votes = len(members)
147
147
148 approvals = sum([
148 approvals = sum([
149 1 for statuses in members.values()
149 1 for statuses in members.values()
150 if statuses and
150 if statuses and
151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
152
152
153 calculated_votes = []
153 calculated_votes = []
154 # we have all votes from users, now check if we have enough votes
154 # we have all votes from users, now check if we have enough votes
155 # to fill other
155 # to fill other
156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
157 if approvals >= required_votes:
157 if approvals >= required_votes:
158 fill_in = ChangesetStatus.STATUS_APPROVED
158 fill_in = ChangesetStatus.STATUS_APPROVED
159
159
160 for member, statuses in members.items():
160 for member, statuses in members.items():
161 if statuses:
161 if statuses:
162 ver, latest = statuses[0]
162 ver, latest = statuses[0]
163 if fill_in == ChangesetStatus.STATUS_APPROVED:
163 if fill_in == ChangesetStatus.STATUS_APPROVED:
164 calculated_votes.append(fill_in)
164 calculated_votes.append(fill_in)
165 else:
165 else:
166 calculated_votes.append(latest.status)
166 calculated_votes.append(latest.status)
167 else:
167 else:
168 calculated_votes.append(fill_in)
168 calculated_votes.append(fill_in)
169
169
170 return calculated_votes
170 return calculated_votes
171
171
172 def calculate_status(self, statuses_by_reviewers):
172 def calculate_status(self, statuses_by_reviewers):
173 """
173 """
174 Given the approval statuses from reviewers, calculates final approval
174 Given the approval statuses from reviewers, calculates final approval
175 status. There can only be 3 results, all approved, all rejected. If
175 status. There can only be 3 results, all approved, all rejected. If
176 there is no consensus the PR is under review.
176 there is no consensus the PR is under review.
177
177
178 :param statuses_by_reviewers:
178 :param statuses_by_reviewers:
179 """
179 """
180
180
181 def group_rule(element):
181 def group_rule(element):
182 review_obj = element[0]
182 review_obj = element[0]
183 rule_data = review_obj.rule_user_group_data()
183 rule_data = review_obj.rule_user_group_data()
184 if rule_data and rule_data['id']:
184 if rule_data and rule_data['id']:
185 return rule_data['id']
185 return rule_data['id']
186
186
187 voting_groups = itertools.groupby(
187 voting_groups = itertools.groupby(
188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
189
189
190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
191
191
192 reviewers_number = len(statuses_by_reviewers)
192 reviewers_number = len(statuses_by_reviewers)
193 votes = collections.defaultdict(int)
193 votes = collections.defaultdict(int)
194 for group, group_statuses_by_reviewers in voting_by_groups:
194 for group, group_statuses_by_reviewers in voting_by_groups:
195 if group:
195 if group:
196 # calculate how the "group" voted
196 # calculate how the "group" voted
197 for vote_status in self.calculate_group_vote(
197 for vote_status in self.calculate_group_vote(
198 group, group_statuses_by_reviewers):
198 group, group_statuses_by_reviewers):
199 votes[vote_status] += 1
199 votes[vote_status] += 1
200 else:
200 else:
201
201
202 for review_obj, user, reasons, mandatory, statuses \
202 for review_obj, user, reasons, mandatory, statuses \
203 in group_statuses_by_reviewers:
203 in group_statuses_by_reviewers:
204 # individual vote
204 # individual vote
205 if statuses:
205 if statuses:
206 ver, latest = statuses[0]
206 ver, latest = statuses[0]
207 votes[latest.status] += 1
207 votes[latest.status] += 1
208
208
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
211
211
212 # TODO(marcink): with group voting, how does rejected work,
212 # TODO(marcink): with group voting, how does rejected work,
213 # do we ever get rejected state ?
213 # do we ever get rejected state ?
214
214
215 if approved_votes_count and (approved_votes_count == reviewers_number):
215 if approved_votes_count and (approved_votes_count == reviewers_number):
216 return ChangesetStatus.STATUS_APPROVED
216 return ChangesetStatus.STATUS_APPROVED
217
217
218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
219 return ChangesetStatus.STATUS_REJECTED
219 return ChangesetStatus.STATUS_REJECTED
220
220
221 return ChangesetStatus.STATUS_UNDER_REVIEW
221 return ChangesetStatus.STATUS_UNDER_REVIEW
222
222
223 def get_statuses(self, repo, revision=None, pull_request=None,
223 def get_statuses(self, repo, revision=None, pull_request=None,
224 with_revisions=False):
224 with_revisions=False):
225 q = self._get_status_query(repo, revision, pull_request,
225 q = self._get_status_query(repo, revision, pull_request,
226 with_revisions)
226 with_revisions)
227 return q.all()
227 return q.all()
228
228
229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
230 """
230 """
231 Returns latest status of changeset for given revision or for given
231 Returns latest status of changeset for given revision or for given
232 pull request. Statuses are versioned inside a table itself and
232 pull request. Statuses are versioned inside a table itself and
233 version == 0 is always the current one
233 version == 0 is always the current one
234
234
235 :param repo:
235 :param repo:
236 :param revision: 40char hash or None
236 :param revision: 40char hash or None
237 :param pull_request: pull_request reference
237 :param pull_request: pull_request reference
238 :param as_str: return status as string not object
238 :param as_str: return status as string not object
239 """
239 """
240 q = self._get_status_query(repo, revision, pull_request)
240 q = self._get_status_query(repo, revision, pull_request)
241
241
242 # need to use first here since there can be multiple statuses
242 # need to use first here since there can be multiple statuses
243 # returned from pull_request
243 # returned from pull_request
244 status = q.first()
244 status = q.first()
245 if as_str:
245 if as_str:
246 status = status.status if status else status
246 status = status.status if status else status
247 st = status or ChangesetStatus.DEFAULT
247 st = status or ChangesetStatus.DEFAULT
248 return str(st)
248 return str(st)
249 return status
249 return status
250
250
251 def _render_auto_status_message(
251 def _render_auto_status_message(
252 self, status, commit_id=None, pull_request=None):
252 self, status, commit_id=None, pull_request=None):
253 """
253 """
254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
255 so it's always looking the same disregarding on which default
255 so it's always looking the same disregarding on which default
256 renderer system is using.
256 renderer system is using.
257
257
258 :param status: status text to change into
258 :param status: status text to change into
259 :param commit_id: the commit_id we change the status for
259 :param commit_id: the commit_id we change the status for
260 :param pull_request: the pull request we change the status for
260 :param pull_request: the pull request we change the status for
261 """
261 """
262
262
263 new_status = ChangesetStatus.get_status_lbl(status)
263 new_status = ChangesetStatus.get_status_lbl(status)
264
264
265 params = {
265 params = {
266 'new_status_label': new_status,
266 'new_status_label': new_status,
267 'pull_request': pull_request,
267 'pull_request': pull_request,
268 'commit_id': commit_id,
268 'commit_id': commit_id,
269 }
269 }
270 renderer = RstTemplateRenderer()
270 renderer = RstTemplateRenderer()
271 return renderer.render('auto_status_change.mako', **params)
271 return renderer.render('auto_status_change.mako', **params)
272
272
273 def set_status(self, repo, status, user, comment=None, revision=None,
273 def set_status(self, repo, status, user, comment=None, revision=None,
274 pull_request=None, dont_allow_on_closed_pull_request=False):
274 pull_request=None, dont_allow_on_closed_pull_request=False):
275 """
275 """
276 Creates new status for changeset or updates the old ones bumping their
276 Creates new status for changeset or updates the old ones bumping their
277 version, leaving the current status at
277 version, leaving the current status at
278
278
279 :param repo:
279 :param repo:
280 :param revision:
280 :param revision:
281 :param status:
281 :param status:
282 :param user:
282 :param user:
283 :param comment:
283 :param comment:
284 :param dont_allow_on_closed_pull_request: don't allow a status change
284 :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
285 if last status was for pull request and it's closed. We shouldn't
286 mess around this manually
286 mess around this manually
287 """
287 """
288 repo = self._get_repo(repo)
288 repo = self._get_repo(repo)
289
289
290 q = ChangesetStatus.query()
290 q = ChangesetStatus.query()
291
291
292 if revision:
292 if revision:
293 q = q.filter(ChangesetStatus.repo == repo)
293 q = q.filter(ChangesetStatus.repo == repo)
294 q = q.filter(ChangesetStatus.revision == revision)
294 q = q.filter(ChangesetStatus.revision == revision)
295 elif pull_request:
295 elif pull_request:
296 pull_request = self.__get_pull_request(pull_request)
296 pull_request = self.__get_pull_request(pull_request)
297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
299 cur_statuses = q.all()
299 cur_statuses = q.all()
300
300
301 # if statuses exists and last is associated with a closed pull request
301 # if statuses exists and last is associated with a closed pull request
302 # we need to check if we can allow this status change
302 # we need to check if we can allow this status change
303 if (dont_allow_on_closed_pull_request and cur_statuses
303 if (dont_allow_on_closed_pull_request and cur_statuses
304 and getattr(cur_statuses[0].pull_request, 'status', '')
304 and getattr(cur_statuses[0].pull_request, 'status', '')
305 == PullRequest.STATUS_CLOSED):
305 == PullRequest.STATUS_CLOSED):
306 raise StatusChangeOnClosedPullRequestError(
306 raise StatusChangeOnClosedPullRequestError(
307 'Changing status on closed pull request is not allowed'
307 'Changing status on closed pull request is not allowed'
308 )
308 )
309
309
310 # update all current statuses with older version
310 # update all current statuses with older version
311 if cur_statuses:
311 if cur_statuses:
312 for st in cur_statuses:
312 for st in cur_statuses:
313 st.version += 1
313 st.version += 1
314 Session().add(st)
314 Session().add(st)
315 Session().flush()
315 Session().flush()
316
316
317 def _create_status(user, repo, status, comment, revision, pull_request):
317 def _create_status(user, repo, status, comment, revision, pull_request):
318 new_status = ChangesetStatus()
318 new_status = ChangesetStatus()
319 new_status.author = self._get_user(user)
319 new_status.author = self._get_user(user)
320 new_status.repo = self._get_repo(repo)
320 new_status.repo = self._get_repo(repo)
321 new_status.status = status
321 new_status.status = status
322 new_status.comment = comment
322 new_status.comment = comment
323 new_status.revision = revision
323 new_status.revision = revision
324 new_status.pull_request = pull_request
324 new_status.pull_request = pull_request
325 return new_status
325 return new_status
326
326
327 if not comment:
327 if not comment:
328 from rhodecode.model.comment import CommentsModel
328 from rhodecode.model.comment import CommentsModel
329 comment = CommentsModel().create(
329 comment = CommentsModel().create(
330 text=self._render_auto_status_message(
330 text=self._render_auto_status_message(
331 status, commit_id=revision, pull_request=pull_request),
331 status, commit_id=revision, pull_request=pull_request),
332 repo=repo,
332 repo=repo,
333 user=user,
333 user=user,
334 pull_request=pull_request,
334 pull_request=pull_request,
335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
336 )
336 )
337
337
338 if revision:
338 if revision:
339 new_status = _create_status(
339 new_status = _create_status(
340 user=user, repo=repo, status=status, comment=comment,
340 user=user, repo=repo, status=status, comment=comment,
341 revision=revision, pull_request=pull_request)
341 revision=revision, pull_request=pull_request)
342 Session().add(new_status)
342 Session().add(new_status)
343 return new_status
343 return new_status
344 elif pull_request:
344 elif pull_request:
345 # pull request can have more than one revision associated to it
345 # pull request can have more than one revision associated to it
346 # we need to create new version for each one
346 # we need to create new version for each one
347 new_statuses = []
347 new_statuses = []
348 repo = pull_request.source_repo
348 repo = pull_request.source_repo
349 for rev in pull_request.revisions:
349 for rev in pull_request.revisions:
350 new_status = _create_status(
350 new_status = _create_status(
351 user=user, repo=repo, status=status, comment=comment,
351 user=user, repo=repo, status=status, comment=comment,
352 revision=rev, pull_request=pull_request)
352 revision=rev, pull_request=pull_request)
353 new_statuses.append(new_status)
353 new_statuses.append(new_status)
354 Session().add(new_status)
354 Session().add(new_status)
355 return new_statuses
355 return new_statuses
356
356
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
358
358
359 commit_statuses_map = collections.defaultdict(list)
359 commit_statuses_map = collections.defaultdict(list)
360 for st in commit_statuses:
360 for st in commit_statuses:
361 commit_statuses_map[st.author.username] += [st]
361 commit_statuses_map[st.author.username] += [st]
362
362
363 reviewers = []
363 reviewers = []
364
364
365 def version(commit_status):
365 def version(commit_status):
366 return commit_status.version
366 return commit_status.version
367
367
368 for obj in reviewers_data:
368 for obj in reviewers_data:
369 if not obj.user:
369 if not obj.user:
370 continue
370 continue
371 statuses = commit_statuses_map.get(obj.user.username, None)
371 statuses = commit_statuses_map.get(obj.user.username, None)
372 if statuses:
372 if statuses:
373 status_groups = itertools.groupby(
373 status_groups = itertools.groupby(
374 sorted(statuses, key=version), version)
374 sorted(statuses, key=version), version)
375 statuses = [(x, list(y)[0]) for x, y in status_groups]
375 statuses = [(x, list(y)[0]) for x, y in status_groups]
376
376
377 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
377 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
378
378
379 return reviewers
379 return reviewers
380
380
381 def reviewers_statuses(self, pull_request):
381 def reviewers_statuses(self, pull_request):
382 _commit_statuses = self.get_statuses(
382 _commit_statuses = self.get_statuses(
383 pull_request.source_repo,
383 pull_request.source_repo,
384 pull_request=pull_request,
384 pull_request=pull_request,
385 with_revisions=True)
385 with_revisions=True)
386 reviewers = pull_request.get_pull_request_reviewers(
387 role=PullRequestReviewers.ROLE_REVIEWER)
388 return self.aggregate_votes_by_user(_commit_statuses, reviewers)
386
389
387 return self.aggregate_votes_by_user(_commit_statuses, pull_request.reviewers)
390 def calculated_review_status(self, pull_request):
388
389 def calculated_review_status(self, pull_request, reviewers_statuses=None):
390 """
391 """
391 calculate pull request status based on reviewers, it should be a list
392 calculate pull request status based on reviewers, it should be a list
392 of two element lists.
393 of two element lists.
393
394 :param reviewers_statuses:
395 """
394 """
396 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
395 reviewers = self.reviewers_statuses(pull_request)
397 return self.calculate_status(reviewers)
396 return self.calculate_status(reviewers)
@@ -1,863 +1,862 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
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 ChangesetComment,
40 ChangesetComment,
41 User,
41 User,
42 Notification,
42 Notification,
43 PullRequest,
43 PullRequest,
44 AttributeDict,
44 AttributeDict,
45 ChangesetCommentHistory,
45 ChangesetCommentHistory,
46 )
46 )
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.validation_schema.schemas import comment_schema
51 from rhodecode.model.validation_schema.schemas import comment_schema
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class CommentsModel(BaseModel):
57 class CommentsModel(BaseModel):
58
58
59 cls = ChangesetComment
59 cls = ChangesetComment
60
60
61 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_AFTER = 3
62 DIFF_CONTEXT_AFTER = 3
63
63
64 def __get_commit_comment(self, changeset_comment):
64 def __get_commit_comment(self, changeset_comment):
65 return self._get_instance(ChangesetComment, changeset_comment)
65 return self._get_instance(ChangesetComment, changeset_comment)
66
66
67 def __get_pull_request(self, pull_request):
67 def __get_pull_request(self, pull_request):
68 return self._get_instance(PullRequest, pull_request)
68 return self._get_instance(PullRequest, pull_request)
69
69
70 def _extract_mentions(self, s):
70 def _extract_mentions(self, s):
71 user_objects = []
71 user_objects = []
72 for username in extract_mentioned_users(s):
72 for username in extract_mentioned_users(s):
73 user_obj = User.get_by_username(username, case_insensitive=True)
73 user_obj = User.get_by_username(username, case_insensitive=True)
74 if user_obj:
74 if user_obj:
75 user_objects.append(user_obj)
75 user_objects.append(user_obj)
76 return user_objects
76 return user_objects
77
77
78 def _get_renderer(self, global_renderer='rst', request=None):
78 def _get_renderer(self, global_renderer='rst', request=None):
79 request = request or get_current_request()
79 request = request or get_current_request()
80
80
81 try:
81 try:
82 global_renderer = request.call_context.visual.default_renderer
82 global_renderer = request.call_context.visual.default_renderer
83 except AttributeError:
83 except AttributeError:
84 log.debug("Renderer not set, falling back "
84 log.debug("Renderer not set, falling back "
85 "to default renderer '%s'", global_renderer)
85 "to default renderer '%s'", global_renderer)
86 except Exception:
86 except Exception:
87 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
88 return global_renderer
88 return global_renderer
89
89
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
92
92
93 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95
95
96 def yield_comments(pos):
96 def yield_comments(pos):
97 for co in comment_groups[pos]:
97 for co in comment_groups[pos]:
98 yield co
98 yield co
99
99
100 comment_versions = collections.defaultdict(
100 comment_versions = collections.defaultdict(
101 lambda: collections.defaultdict(list))
101 lambda: collections.defaultdict(list))
102 prev_prvid = -1
102 prev_prvid = -1
103 # fake last entry with None, to aggregate on "latest" version which
103 # fake last entry with None, to aggregate on "latest" version which
104 # doesn't have an pull_request_version_id
104 # doesn't have an pull_request_version_id
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 prvid = ver.pull_request_version_id
106 prvid = ver.pull_request_version_id
107 if prev_prvid == -1:
107 if prev_prvid == -1:
108 prev_prvid = prvid
108 prev_prvid = prvid
109
109
110 for co in yield_comments(prvid):
110 for co in yield_comments(prvid):
111 comment_versions[prvid]['at'].append(co)
111 comment_versions[prvid]['at'].append(co)
112
112
113 # save until
113 # save until
114 current = comment_versions[prvid]['at']
114 current = comment_versions[prvid]['at']
115 prev_until = comment_versions[prev_prvid]['until']
115 prev_until = comment_versions[prev_prvid]['until']
116 cur_until = prev_until + current
116 cur_until = prev_until + current
117 comment_versions[prvid]['until'].extend(cur_until)
117 comment_versions[prvid]['until'].extend(cur_until)
118
118
119 # save outdated
119 # save outdated
120 if inline:
120 if inline:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.outdated_at_version(show_version)]
122 if x.outdated_at_version(show_version)]
123 else:
123 else:
124 outdated = [x for x in cur_until
124 outdated = [x for x in cur_until
125 if x.older_than_version(show_version)]
125 if x.older_than_version(show_version)]
126 display = [x for x in cur_until if x not in outdated]
126 display = [x for x in cur_until if x not in outdated]
127
127
128 comment_versions[prvid]['outdated'] = outdated
128 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['display'] = display
129 comment_versions[prvid]['display'] = display
130
130
131 prev_prvid = prvid
131 prev_prvid = prvid
132
132
133 return comment_versions
133 return comment_versions
134
134
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 qry = Session().query(ChangesetComment) \
136 qry = Session().query(ChangesetComment) \
137 .filter(ChangesetComment.repo == repo)
137 .filter(ChangesetComment.repo == repo)
138
138
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141
141
142 if user:
142 if user:
143 user = self._get_user(user)
143 user = self._get_user(user)
144 if user:
144 if user:
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146
146
147 if commit_id:
147 if commit_id:
148 qry = qry.filter(ChangesetComment.revision == commit_id)
148 qry = qry.filter(ChangesetComment.revision == commit_id)
149
149
150 qry = qry.order_by(ChangesetComment.created_on)
150 qry = qry.order_by(ChangesetComment.created_on)
151 return qry.all()
151 return qry.all()
152
152
153 def get_repository_unresolved_todos(self, repo):
153 def get_repository_unresolved_todos(self, repo):
154 todos = Session().query(ChangesetComment) \
154 todos = Session().query(ChangesetComment) \
155 .filter(ChangesetComment.repo == repo) \
155 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.resolved_by == None) \
156 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.comment_type
157 .filter(ChangesetComment.comment_type
158 == ChangesetComment.COMMENT_TYPE_TODO)
158 == ChangesetComment.COMMENT_TYPE_TODO)
159 todos = todos.all()
159 todos = todos.all()
160
160
161 return todos
161 return todos
162
162
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164
164
165 todos = Session().query(ChangesetComment) \
165 todos = Session().query(ChangesetComment) \
166 .filter(ChangesetComment.pull_request == pull_request) \
166 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.resolved_by == None) \
167 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.comment_type
168 .filter(ChangesetComment.comment_type
169 == ChangesetComment.COMMENT_TYPE_TODO)
169 == ChangesetComment.COMMENT_TYPE_TODO)
170
170
171 if not show_outdated:
171 if not show_outdated:
172 todos = todos.filter(
172 todos = todos.filter(
173 coalesce(ChangesetComment.display_state, '') !=
173 coalesce(ChangesetComment.display_state, '') !=
174 ChangesetComment.COMMENT_OUTDATED)
174 ChangesetComment.COMMENT_OUTDATED)
175
175
176 todos = todos.all()
176 todos = todos.all()
177
177
178 return todos
178 return todos
179
179
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
181
181
182 todos = Session().query(ChangesetComment) \
182 todos = Session().query(ChangesetComment) \
183 .filter(ChangesetComment.pull_request == pull_request) \
183 .filter(ChangesetComment.pull_request == pull_request) \
184 .filter(ChangesetComment.resolved_by != None) \
184 .filter(ChangesetComment.resolved_by != None) \
185 .filter(ChangesetComment.comment_type
185 .filter(ChangesetComment.comment_type
186 == ChangesetComment.COMMENT_TYPE_TODO)
186 == ChangesetComment.COMMENT_TYPE_TODO)
187
187
188 if not show_outdated:
188 if not show_outdated:
189 todos = todos.filter(
189 todos = todos.filter(
190 coalesce(ChangesetComment.display_state, '') !=
190 coalesce(ChangesetComment.display_state, '') !=
191 ChangesetComment.COMMENT_OUTDATED)
191 ChangesetComment.COMMENT_OUTDATED)
192
192
193 todos = todos.all()
193 todos = todos.all()
194
194
195 return todos
195 return todos
196
196
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
198
198
199 todos = Session().query(ChangesetComment) \
199 todos = Session().query(ChangesetComment) \
200 .filter(ChangesetComment.revision == commit_id) \
200 .filter(ChangesetComment.revision == commit_id) \
201 .filter(ChangesetComment.resolved_by == None) \
201 .filter(ChangesetComment.resolved_by == None) \
202 .filter(ChangesetComment.comment_type
202 .filter(ChangesetComment.comment_type
203 == ChangesetComment.COMMENT_TYPE_TODO)
203 == ChangesetComment.COMMENT_TYPE_TODO)
204
204
205 if not show_outdated:
205 if not show_outdated:
206 todos = todos.filter(
206 todos = todos.filter(
207 coalesce(ChangesetComment.display_state, '') !=
207 coalesce(ChangesetComment.display_state, '') !=
208 ChangesetComment.COMMENT_OUTDATED)
208 ChangesetComment.COMMENT_OUTDATED)
209
209
210 todos = todos.all()
210 todos = todos.all()
211
211
212 return todos
212 return todos
213
213
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
215
215
216 todos = Session().query(ChangesetComment) \
216 todos = Session().query(ChangesetComment) \
217 .filter(ChangesetComment.revision == commit_id) \
217 .filter(ChangesetComment.revision == commit_id) \
218 .filter(ChangesetComment.resolved_by != None) \
218 .filter(ChangesetComment.resolved_by != None) \
219 .filter(ChangesetComment.comment_type
219 .filter(ChangesetComment.comment_type
220 == ChangesetComment.COMMENT_TYPE_TODO)
220 == ChangesetComment.COMMENT_TYPE_TODO)
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_inline_comments(self, commit_id):
231 def get_commit_inline_comments(self, commit_id):
232 inline_comments = Session().query(ChangesetComment) \
232 inline_comments = Session().query(ChangesetComment) \
233 .filter(ChangesetComment.line_no != None) \
233 .filter(ChangesetComment.line_no != None) \
234 .filter(ChangesetComment.f_path != None) \
234 .filter(ChangesetComment.f_path != None) \
235 .filter(ChangesetComment.revision == commit_id)
235 .filter(ChangesetComment.revision == commit_id)
236 inline_comments = inline_comments.all()
236 inline_comments = inline_comments.all()
237 return inline_comments
237 return inline_comments
238
238
239 def _log_audit_action(self, action, action_data, auth_user, comment):
239 def _log_audit_action(self, action, action_data, auth_user, comment):
240 audit_logger.store(
240 audit_logger.store(
241 action=action,
241 action=action,
242 action_data=action_data,
242 action_data=action_data,
243 user=auth_user,
243 user=auth_user,
244 repo=comment.repo)
244 repo=comment.repo)
245
245
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
247 f_path=None, line_no=None, status_change=None,
247 f_path=None, line_no=None, status_change=None,
248 status_change_type=None, comment_type=None,
248 status_change_type=None, comment_type=None,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
250 renderer=None, auth_user=None, extra_recipients=None):
250 renderer=None, auth_user=None, extra_recipients=None):
251 """
251 """
252 Creates new comment for commit or pull request.
252 Creates new comment for commit or pull request.
253 IF status_change is not none this comment is associated with a
253 IF status_change is not none this comment is associated with a
254 status change of commit or commit associated with pull request
254 status change of commit or commit associated with pull request
255
255
256 :param text:
256 :param text:
257 :param repo:
257 :param repo:
258 :param user:
258 :param user:
259 :param commit_id:
259 :param commit_id:
260 :param pull_request:
260 :param pull_request:
261 :param f_path:
261 :param f_path:
262 :param line_no:
262 :param line_no:
263 :param status_change: Label for status change
263 :param status_change: Label for status change
264 :param comment_type: Type of comment
264 :param comment_type: Type of comment
265 :param resolves_comment_id: id of comment which this one will resolve
265 :param resolves_comment_id: id of comment which this one will resolve
266 :param status_change_type: type of status change
266 :param status_change_type: type of status change
267 :param closing_pr:
267 :param closing_pr:
268 :param send_email:
268 :param send_email:
269 :param renderer: pick renderer for this comment
269 :param renderer: pick renderer for this comment
270 :param auth_user: current authenticated user calling this method
270 :param auth_user: current authenticated user calling this method
271 :param extra_recipients: list of extra users to be added to recipients
271 :param extra_recipients: list of extra users to be added to recipients
272 """
272 """
273
273
274 if not text:
274 if not text:
275 log.warning('Missing text for comment, skipping...')
275 log.warning('Missing text for comment, skipping...')
276 return
276 return
277 request = get_current_request()
277 request = get_current_request()
278 _ = request.translate
278 _ = request.translate
279
279
280 if not renderer:
280 if not renderer:
281 renderer = self._get_renderer(request=request)
281 renderer = self._get_renderer(request=request)
282
282
283 repo = self._get_repo(repo)
283 repo = self._get_repo(repo)
284 user = self._get_user(user)
284 user = self._get_user(user)
285 auth_user = auth_user or user
285 auth_user = auth_user or user
286
286
287 schema = comment_schema.CommentSchema()
287 schema = comment_schema.CommentSchema()
288 validated_kwargs = schema.deserialize(dict(
288 validated_kwargs = schema.deserialize(dict(
289 comment_body=text,
289 comment_body=text,
290 comment_type=comment_type,
290 comment_type=comment_type,
291 comment_file=f_path,
291 comment_file=f_path,
292 comment_line=line_no,
292 comment_line=line_no,
293 renderer_type=renderer,
293 renderer_type=renderer,
294 status_change=status_change_type,
294 status_change=status_change_type,
295 resolves_comment_id=resolves_comment_id,
295 resolves_comment_id=resolves_comment_id,
296 repo=repo.repo_id,
296 repo=repo.repo_id,
297 user=user.user_id,
297 user=user.user_id,
298 ))
298 ))
299
299
300 comment = ChangesetComment()
300 comment = ChangesetComment()
301 comment.renderer = validated_kwargs['renderer_type']
301 comment.renderer = validated_kwargs['renderer_type']
302 comment.text = validated_kwargs['comment_body']
302 comment.text = validated_kwargs['comment_body']
303 comment.f_path = validated_kwargs['comment_file']
303 comment.f_path = validated_kwargs['comment_file']
304 comment.line_no = validated_kwargs['comment_line']
304 comment.line_no = validated_kwargs['comment_line']
305 comment.comment_type = validated_kwargs['comment_type']
305 comment.comment_type = validated_kwargs['comment_type']
306
306
307 comment.repo = repo
307 comment.repo = repo
308 comment.author = user
308 comment.author = user
309 resolved_comment = self.__get_commit_comment(
309 resolved_comment = self.__get_commit_comment(
310 validated_kwargs['resolves_comment_id'])
310 validated_kwargs['resolves_comment_id'])
311 # check if the comment actually belongs to this PR
311 # check if the comment actually belongs to this PR
312 if resolved_comment and resolved_comment.pull_request and \
312 if resolved_comment and resolved_comment.pull_request and \
313 resolved_comment.pull_request != pull_request:
313 resolved_comment.pull_request != pull_request:
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
315 resolved_comment)
315 resolved_comment)
316 # comment not bound to this pull request, forbid
316 # comment not bound to this pull request, forbid
317 resolved_comment = None
317 resolved_comment = None
318
318
319 elif resolved_comment and resolved_comment.repo and \
319 elif resolved_comment and resolved_comment.repo and \
320 resolved_comment.repo != repo:
320 resolved_comment.repo != repo:
321 log.warning('Comment tried to resolved unrelated todo comment: %s',
321 log.warning('Comment tried to resolved unrelated todo comment: %s',
322 resolved_comment)
322 resolved_comment)
323 # comment not bound to this repo, forbid
323 # comment not bound to this repo, forbid
324 resolved_comment = None
324 resolved_comment = None
325
325
326 comment.resolved_comment = resolved_comment
326 comment.resolved_comment = resolved_comment
327
327
328 pull_request_id = pull_request
328 pull_request_id = pull_request
329
329
330 commit_obj = None
330 commit_obj = None
331 pull_request_obj = None
331 pull_request_obj = None
332
332
333 if commit_id:
333 if commit_id:
334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
335 # do a lookup, so we don't pass something bad here
335 # do a lookup, so we don't pass something bad here
336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
337 comment.revision = commit_obj.raw_id
337 comment.revision = commit_obj.raw_id
338
338
339 elif pull_request_id:
339 elif pull_request_id:
340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
341 pull_request_obj = self.__get_pull_request(pull_request_id)
341 pull_request_obj = self.__get_pull_request(pull_request_id)
342 comment.pull_request = pull_request_obj
342 comment.pull_request = pull_request_obj
343 else:
343 else:
344 raise Exception('Please specify commit or pull_request_id')
344 raise Exception('Please specify commit or pull_request_id')
345
345
346 Session().add(comment)
346 Session().add(comment)
347 Session().flush()
347 Session().flush()
348 kwargs = {
348 kwargs = {
349 'user': user,
349 'user': user,
350 'renderer_type': renderer,
350 'renderer_type': renderer,
351 'repo_name': repo.repo_name,
351 'repo_name': repo.repo_name,
352 'status_change': status_change,
352 'status_change': status_change,
353 'status_change_type': status_change_type,
353 'status_change_type': status_change_type,
354 'comment_body': text,
354 'comment_body': text,
355 'comment_file': f_path,
355 'comment_file': f_path,
356 'comment_line': line_no,
356 'comment_line': line_no,
357 'comment_type': comment_type or 'note',
357 'comment_type': comment_type or 'note',
358 'comment_id': comment.comment_id
358 'comment_id': comment.comment_id
359 }
359 }
360
360
361 if commit_obj:
361 if commit_obj:
362 recipients = ChangesetComment.get_users(
362 recipients = ChangesetComment.get_users(
363 revision=commit_obj.raw_id)
363 revision=commit_obj.raw_id)
364 # add commit author if it's in RhodeCode system
364 # add commit author if it's in RhodeCode system
365 cs_author = User.get_from_cs_author(commit_obj.author)
365 cs_author = User.get_from_cs_author(commit_obj.author)
366 if not cs_author:
366 if not cs_author:
367 # use repo owner if we cannot extract the author correctly
367 # use repo owner if we cannot extract the author correctly
368 cs_author = repo.user
368 cs_author = repo.user
369 recipients += [cs_author]
369 recipients += [cs_author]
370
370
371 commit_comment_url = self.get_url(comment, request=request)
371 commit_comment_url = self.get_url(comment, request=request)
372 commit_comment_reply_url = self.get_url(
372 commit_comment_reply_url = self.get_url(
373 comment, request=request,
373 comment, request=request,
374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
375
375
376 target_repo_url = h.link_to(
376 target_repo_url = h.link_to(
377 repo.repo_name,
377 repo.repo_name,
378 h.route_url('repo_summary', repo_name=repo.repo_name))
378 h.route_url('repo_summary', repo_name=repo.repo_name))
379
379
380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
381 commit_id=commit_id)
381 commit_id=commit_id)
382
382
383 # commit specifics
383 # commit specifics
384 kwargs.update({
384 kwargs.update({
385 'commit': commit_obj,
385 'commit': commit_obj,
386 'commit_message': commit_obj.message,
386 'commit_message': commit_obj.message,
387 'commit_target_repo_url': target_repo_url,
387 'commit_target_repo_url': target_repo_url,
388 'commit_comment_url': commit_comment_url,
388 'commit_comment_url': commit_comment_url,
389 'commit_comment_reply_url': commit_comment_reply_url,
389 'commit_comment_reply_url': commit_comment_reply_url,
390 'commit_url': commit_url,
390 'commit_url': commit_url,
391 'thread_ids': [commit_url, commit_comment_url],
391 'thread_ids': [commit_url, commit_comment_url],
392 })
392 })
393
393
394 elif pull_request_obj:
394 elif pull_request_obj:
395 # get the current participants of this pull request
395 # get the current participants of this pull request
396 recipients = ChangesetComment.get_users(
396 recipients = ChangesetComment.get_users(
397 pull_request_id=pull_request_obj.pull_request_id)
397 pull_request_id=pull_request_obj.pull_request_id)
398 # add pull request author
398 # add pull request author
399 recipients += [pull_request_obj.author]
399 recipients += [pull_request_obj.author]
400
400
401 # add the reviewers to notification
401 # add the reviewers to notification
402 recipients += [x.user for x in pull_request_obj.reviewers]
402 recipients += [x.user for x in pull_request_obj.reviewers]
403
403
404 pr_target_repo = pull_request_obj.target_repo
404 pr_target_repo = pull_request_obj.target_repo
405 pr_source_repo = pull_request_obj.source_repo
405 pr_source_repo = pull_request_obj.source_repo
406
406
407 pr_comment_url = self.get_url(comment, request=request)
407 pr_comment_url = self.get_url(comment, request=request)
408 pr_comment_reply_url = self.get_url(
408 pr_comment_reply_url = self.get_url(
409 comment, request=request,
409 comment, request=request,
410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
411
411
412 pr_url = h.route_url(
412 pr_url = h.route_url(
413 'pullrequest_show',
413 'pullrequest_show',
414 repo_name=pr_target_repo.repo_name,
414 repo_name=pr_target_repo.repo_name,
415 pull_request_id=pull_request_obj.pull_request_id, )
415 pull_request_id=pull_request_obj.pull_request_id, )
416
416
417 # set some variables for email notification
417 # set some variables for email notification
418 pr_target_repo_url = h.route_url(
418 pr_target_repo_url = h.route_url(
419 'repo_summary', repo_name=pr_target_repo.repo_name)
419 'repo_summary', repo_name=pr_target_repo.repo_name)
420
420
421 pr_source_repo_url = h.route_url(
421 pr_source_repo_url = h.route_url(
422 'repo_summary', repo_name=pr_source_repo.repo_name)
422 'repo_summary', repo_name=pr_source_repo.repo_name)
423
423
424 # pull request specifics
424 # pull request specifics
425 kwargs.update({
425 kwargs.update({
426 'pull_request': pull_request_obj,
426 'pull_request': pull_request_obj,
427 'pr_id': pull_request_obj.pull_request_id,
427 'pr_id': pull_request_obj.pull_request_id,
428 'pull_request_url': pr_url,
428 'pull_request_url': pr_url,
429 'pull_request_target_repo': pr_target_repo,
429 'pull_request_target_repo': pr_target_repo,
430 'pull_request_target_repo_url': pr_target_repo_url,
430 'pull_request_target_repo_url': pr_target_repo_url,
431 'pull_request_source_repo': pr_source_repo,
431 'pull_request_source_repo': pr_source_repo,
432 'pull_request_source_repo_url': pr_source_repo_url,
432 'pull_request_source_repo_url': pr_source_repo_url,
433 'pr_comment_url': pr_comment_url,
433 'pr_comment_url': pr_comment_url,
434 'pr_comment_reply_url': pr_comment_reply_url,
434 'pr_comment_reply_url': pr_comment_reply_url,
435 'pr_closing': closing_pr,
435 'pr_closing': closing_pr,
436 'thread_ids': [pr_url, pr_comment_url],
436 'thread_ids': [pr_url, pr_comment_url],
437 })
437 })
438
438
439 recipients += [self._get_user(u) for u in (extra_recipients or [])]
440
441 if send_email:
439 if send_email:
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
442 # pre-generate the subject for notification itself
441 # pre-generate the subject for notification itself
443 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
444 notification_type, **kwargs)
443 notification_type, **kwargs)
445
444
446 mention_recipients = set(
445 mention_recipients = set(
447 self._extract_mentions(text)).difference(recipients)
446 self._extract_mentions(text)).difference(recipients)
448
447
449 # create notification objects, and emails
448 # create notification objects, and emails
450 NotificationModel().create(
449 NotificationModel().create(
451 created_by=user,
450 created_by=user,
452 notification_subject=subject,
451 notification_subject=subject,
453 notification_body=body_plaintext,
452 notification_body=body_plaintext,
454 notification_type=notification_type,
453 notification_type=notification_type,
455 recipients=recipients,
454 recipients=recipients,
456 mention_recipients=mention_recipients,
455 mention_recipients=mention_recipients,
457 email_kwargs=kwargs,
456 email_kwargs=kwargs,
458 )
457 )
459
458
460 Session().flush()
459 Session().flush()
461 if comment.pull_request:
460 if comment.pull_request:
462 action = 'repo.pull_request.comment.create'
461 action = 'repo.pull_request.comment.create'
463 else:
462 else:
464 action = 'repo.commit.comment.create'
463 action = 'repo.commit.comment.create'
465
464
466 comment_id = comment.comment_id
465 comment_id = comment.comment_id
467 comment_data = comment.get_api_data()
466 comment_data = comment.get_api_data()
468
467
469 self._log_audit_action(
468 self._log_audit_action(
470 action, {'data': comment_data}, auth_user, comment)
469 action, {'data': comment_data}, auth_user, comment)
471
470
472 channel = None
471 channel = None
473 if commit_obj:
472 if commit_obj:
474 repo_name = repo.repo_name
473 repo_name = repo.repo_name
475 channel = u'/repo${}$/commit/{}'.format(
474 channel = u'/repo${}$/commit/{}'.format(
476 repo_name,
475 repo_name,
477 commit_obj.raw_id
476 commit_obj.raw_id
478 )
477 )
479 elif pull_request_obj:
478 elif pull_request_obj:
480 repo_name = pr_target_repo.repo_name
479 repo_name = pr_target_repo.repo_name
481 channel = u'/repo${}$/pr/{}'.format(
480 channel = u'/repo${}$/pr/{}'.format(
482 repo_name,
481 repo_name,
483 pull_request_obj.pull_request_id
482 pull_request_obj.pull_request_id
484 )
483 )
485
484
486 if channel:
485 if channel:
487 username = user.username
486 username = user.username
488 message = '<strong>{}</strong> {} #{}, {}'
487 message = '<strong>{}</strong> {} #{}, {}'
489 message = message.format(
488 message = message.format(
490 username,
489 username,
491 _('posted a new comment'),
490 _('posted a new comment'),
492 comment_id,
491 comment_id,
493 _('Refresh the page to see new comments.'))
492 _('Refresh the page to see new comments.'))
494
493
495 message_obj = {
494 message_obj = {
496 'message': message,
495 'message': message,
497 'level': 'success',
496 'level': 'success',
498 'topic': '/notifications'
497 'topic': '/notifications'
499 }
498 }
500
499
501 channelstream.post_message(
500 channelstream.post_message(
502 channel, message_obj, user.username,
501 channel, message_obj, user.username,
503 registry=get_current_registry())
502 registry=get_current_registry())
504
503
505 message_obj = {
504 message_obj = {
506 'message': None,
505 'message': None,
507 'user': username,
506 'user': username,
508 'comment_id': comment_id,
507 'comment_id': comment_id,
509 'topic': '/comment'
508 'topic': '/comment'
510 }
509 }
511 channelstream.post_message(
510 channelstream.post_message(
512 channel, message_obj, user.username,
511 channel, message_obj, user.username,
513 registry=get_current_registry())
512 registry=get_current_registry())
514
513
515 return comment
514 return comment
516
515
517 def edit(self, comment_id, text, auth_user, version):
516 def edit(self, comment_id, text, auth_user, version):
518 """
517 """
519 Change existing comment for commit or pull request.
518 Change existing comment for commit or pull request.
520
519
521 :param comment_id:
520 :param comment_id:
522 :param text:
521 :param text:
523 :param auth_user: current authenticated user calling this method
522 :param auth_user: current authenticated user calling this method
524 :param version: last comment version
523 :param version: last comment version
525 """
524 """
526 if not text:
525 if not text:
527 log.warning('Missing text for comment, skipping...')
526 log.warning('Missing text for comment, skipping...')
528 return
527 return
529
528
530 comment = ChangesetComment.get(comment_id)
529 comment = ChangesetComment.get(comment_id)
531 old_comment_text = comment.text
530 old_comment_text = comment.text
532 comment.text = text
531 comment.text = text
533 comment.modified_at = datetime.datetime.now()
532 comment.modified_at = datetime.datetime.now()
534 version = safe_int(version)
533 version = safe_int(version)
535
534
536 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
535 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
537 # would return 3 here
536 # would return 3 here
538 comment_version = ChangesetCommentHistory.get_version(comment_id)
537 comment_version = ChangesetCommentHistory.get_version(comment_id)
539
538
540 if isinstance(version, (int, long)) and (comment_version - version) != 1:
539 if isinstance(version, (int, long)) and (comment_version - version) != 1:
541 log.warning(
540 log.warning(
542 'Version mismatch comment_version {} submitted {}, skipping'.format(
541 'Version mismatch comment_version {} submitted {}, skipping'.format(
543 comment_version-1, # -1 since note above
542 comment_version-1, # -1 since note above
544 version
543 version
545 )
544 )
546 )
545 )
547 raise CommentVersionMismatch()
546 raise CommentVersionMismatch()
548
547
549 comment_history = ChangesetCommentHistory()
548 comment_history = ChangesetCommentHistory()
550 comment_history.comment_id = comment_id
549 comment_history.comment_id = comment_id
551 comment_history.version = comment_version
550 comment_history.version = comment_version
552 comment_history.created_by_user_id = auth_user.user_id
551 comment_history.created_by_user_id = auth_user.user_id
553 comment_history.text = old_comment_text
552 comment_history.text = old_comment_text
554 # TODO add email notification
553 # TODO add email notification
555 Session().add(comment_history)
554 Session().add(comment_history)
556 Session().add(comment)
555 Session().add(comment)
557 Session().flush()
556 Session().flush()
558
557
559 if comment.pull_request:
558 if comment.pull_request:
560 action = 'repo.pull_request.comment.edit'
559 action = 'repo.pull_request.comment.edit'
561 else:
560 else:
562 action = 'repo.commit.comment.edit'
561 action = 'repo.commit.comment.edit'
563
562
564 comment_data = comment.get_api_data()
563 comment_data = comment.get_api_data()
565 comment_data['old_comment_text'] = old_comment_text
564 comment_data['old_comment_text'] = old_comment_text
566 self._log_audit_action(
565 self._log_audit_action(
567 action, {'data': comment_data}, auth_user, comment)
566 action, {'data': comment_data}, auth_user, comment)
568
567
569 return comment_history
568 return comment_history
570
569
571 def delete(self, comment, auth_user):
570 def delete(self, comment, auth_user):
572 """
571 """
573 Deletes given comment
572 Deletes given comment
574 """
573 """
575 comment = self.__get_commit_comment(comment)
574 comment = self.__get_commit_comment(comment)
576 old_data = comment.get_api_data()
575 old_data = comment.get_api_data()
577 Session().delete(comment)
576 Session().delete(comment)
578
577
579 if comment.pull_request:
578 if comment.pull_request:
580 action = 'repo.pull_request.comment.delete'
579 action = 'repo.pull_request.comment.delete'
581 else:
580 else:
582 action = 'repo.commit.comment.delete'
581 action = 'repo.commit.comment.delete'
583
582
584 self._log_audit_action(
583 self._log_audit_action(
585 action, {'old_data': old_data}, auth_user, comment)
584 action, {'old_data': old_data}, auth_user, comment)
586
585
587 return comment
586 return comment
588
587
589 def get_all_comments(self, repo_id, revision=None, pull_request=None):
588 def get_all_comments(self, repo_id, revision=None, pull_request=None):
590 q = ChangesetComment.query()\
589 q = ChangesetComment.query()\
591 .filter(ChangesetComment.repo_id == repo_id)
590 .filter(ChangesetComment.repo_id == repo_id)
592 if revision:
591 if revision:
593 q = q.filter(ChangesetComment.revision == revision)
592 q = q.filter(ChangesetComment.revision == revision)
594 elif pull_request:
593 elif pull_request:
595 pull_request = self.__get_pull_request(pull_request)
594 pull_request = self.__get_pull_request(pull_request)
596 q = q.filter(ChangesetComment.pull_request == pull_request)
595 q = q.filter(ChangesetComment.pull_request == pull_request)
597 else:
596 else:
598 raise Exception('Please specify commit or pull_request')
597 raise Exception('Please specify commit or pull_request')
599 q = q.order_by(ChangesetComment.created_on)
598 q = q.order_by(ChangesetComment.created_on)
600 return q.all()
599 return q.all()
601
600
602 def get_url(self, comment, request=None, permalink=False, anchor=None):
601 def get_url(self, comment, request=None, permalink=False, anchor=None):
603 if not request:
602 if not request:
604 request = get_current_request()
603 request = get_current_request()
605
604
606 comment = self.__get_commit_comment(comment)
605 comment = self.__get_commit_comment(comment)
607 if anchor is None:
606 if anchor is None:
608 anchor = 'comment-{}'.format(comment.comment_id)
607 anchor = 'comment-{}'.format(comment.comment_id)
609
608
610 if comment.pull_request:
609 if comment.pull_request:
611 pull_request = comment.pull_request
610 pull_request = comment.pull_request
612 if permalink:
611 if permalink:
613 return request.route_url(
612 return request.route_url(
614 'pull_requests_global',
613 'pull_requests_global',
615 pull_request_id=pull_request.pull_request_id,
614 pull_request_id=pull_request.pull_request_id,
616 _anchor=anchor)
615 _anchor=anchor)
617 else:
616 else:
618 return request.route_url(
617 return request.route_url(
619 'pullrequest_show',
618 'pullrequest_show',
620 repo_name=safe_str(pull_request.target_repo.repo_name),
619 repo_name=safe_str(pull_request.target_repo.repo_name),
621 pull_request_id=pull_request.pull_request_id,
620 pull_request_id=pull_request.pull_request_id,
622 _anchor=anchor)
621 _anchor=anchor)
623
622
624 else:
623 else:
625 repo = comment.repo
624 repo = comment.repo
626 commit_id = comment.revision
625 commit_id = comment.revision
627
626
628 if permalink:
627 if permalink:
629 return request.route_url(
628 return request.route_url(
630 'repo_commit', repo_name=safe_str(repo.repo_id),
629 'repo_commit', repo_name=safe_str(repo.repo_id),
631 commit_id=commit_id,
630 commit_id=commit_id,
632 _anchor=anchor)
631 _anchor=anchor)
633
632
634 else:
633 else:
635 return request.route_url(
634 return request.route_url(
636 'repo_commit', repo_name=safe_str(repo.repo_name),
635 'repo_commit', repo_name=safe_str(repo.repo_name),
637 commit_id=commit_id,
636 commit_id=commit_id,
638 _anchor=anchor)
637 _anchor=anchor)
639
638
640 def get_comments(self, repo_id, revision=None, pull_request=None):
639 def get_comments(self, repo_id, revision=None, pull_request=None):
641 """
640 """
642 Gets main comments based on revision or pull_request_id
641 Gets main comments based on revision or pull_request_id
643
642
644 :param repo_id:
643 :param repo_id:
645 :param revision:
644 :param revision:
646 :param pull_request:
645 :param pull_request:
647 """
646 """
648
647
649 q = ChangesetComment.query()\
648 q = ChangesetComment.query()\
650 .filter(ChangesetComment.repo_id == repo_id)\
649 .filter(ChangesetComment.repo_id == repo_id)\
651 .filter(ChangesetComment.line_no == None)\
650 .filter(ChangesetComment.line_no == None)\
652 .filter(ChangesetComment.f_path == None)
651 .filter(ChangesetComment.f_path == None)
653 if revision:
652 if revision:
654 q = q.filter(ChangesetComment.revision == revision)
653 q = q.filter(ChangesetComment.revision == revision)
655 elif pull_request:
654 elif pull_request:
656 pull_request = self.__get_pull_request(pull_request)
655 pull_request = self.__get_pull_request(pull_request)
657 q = q.filter(ChangesetComment.pull_request == pull_request)
656 q = q.filter(ChangesetComment.pull_request == pull_request)
658 else:
657 else:
659 raise Exception('Please specify commit or pull_request')
658 raise Exception('Please specify commit or pull_request')
660 q = q.order_by(ChangesetComment.created_on)
659 q = q.order_by(ChangesetComment.created_on)
661 return q.all()
660 return q.all()
662
661
663 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
662 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
664 q = self._get_inline_comments_query(repo_id, revision, pull_request)
663 q = self._get_inline_comments_query(repo_id, revision, pull_request)
665 return self._group_comments_by_path_and_line_number(q)
664 return self._group_comments_by_path_and_line_number(q)
666
665
667 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
666 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
668 version=None):
667 version=None):
669 inline_comms = []
668 inline_comms = []
670 for fname, per_line_comments in inline_comments.iteritems():
669 for fname, per_line_comments in inline_comments.iteritems():
671 for lno, comments in per_line_comments.iteritems():
670 for lno, comments in per_line_comments.iteritems():
672 for comm in comments:
671 for comm in comments:
673 if not comm.outdated_at_version(version) and skip_outdated:
672 if not comm.outdated_at_version(version) and skip_outdated:
674 inline_comms.append(comm)
673 inline_comms.append(comm)
675
674
676 return inline_comms
675 return inline_comms
677
676
678 def get_outdated_comments(self, repo_id, pull_request):
677 def get_outdated_comments(self, repo_id, pull_request):
679 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
678 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
680 # of a pull request.
679 # of a pull request.
681 q = self._all_inline_comments_of_pull_request(pull_request)
680 q = self._all_inline_comments_of_pull_request(pull_request)
682 q = q.filter(
681 q = q.filter(
683 ChangesetComment.display_state ==
682 ChangesetComment.display_state ==
684 ChangesetComment.COMMENT_OUTDATED
683 ChangesetComment.COMMENT_OUTDATED
685 ).order_by(ChangesetComment.comment_id.asc())
684 ).order_by(ChangesetComment.comment_id.asc())
686
685
687 return self._group_comments_by_path_and_line_number(q)
686 return self._group_comments_by_path_and_line_number(q)
688
687
689 def _get_inline_comments_query(self, repo_id, revision, pull_request):
688 def _get_inline_comments_query(self, repo_id, revision, pull_request):
690 # TODO: johbo: Split this into two methods: One for PR and one for
689 # TODO: johbo: Split this into two methods: One for PR and one for
691 # commit.
690 # commit.
692 if revision:
691 if revision:
693 q = Session().query(ChangesetComment).filter(
692 q = Session().query(ChangesetComment).filter(
694 ChangesetComment.repo_id == repo_id,
693 ChangesetComment.repo_id == repo_id,
695 ChangesetComment.line_no != null(),
694 ChangesetComment.line_no != null(),
696 ChangesetComment.f_path != null(),
695 ChangesetComment.f_path != null(),
697 ChangesetComment.revision == revision)
696 ChangesetComment.revision == revision)
698
697
699 elif pull_request:
698 elif pull_request:
700 pull_request = self.__get_pull_request(pull_request)
699 pull_request = self.__get_pull_request(pull_request)
701 if not CommentsModel.use_outdated_comments(pull_request):
700 if not CommentsModel.use_outdated_comments(pull_request):
702 q = self._visible_inline_comments_of_pull_request(pull_request)
701 q = self._visible_inline_comments_of_pull_request(pull_request)
703 else:
702 else:
704 q = self._all_inline_comments_of_pull_request(pull_request)
703 q = self._all_inline_comments_of_pull_request(pull_request)
705
704
706 else:
705 else:
707 raise Exception('Please specify commit or pull_request_id')
706 raise Exception('Please specify commit or pull_request_id')
708 q = q.order_by(ChangesetComment.comment_id.asc())
707 q = q.order_by(ChangesetComment.comment_id.asc())
709 return q
708 return q
710
709
711 def _group_comments_by_path_and_line_number(self, q):
710 def _group_comments_by_path_and_line_number(self, q):
712 comments = q.all()
711 comments = q.all()
713 paths = collections.defaultdict(lambda: collections.defaultdict(list))
712 paths = collections.defaultdict(lambda: collections.defaultdict(list))
714 for co in comments:
713 for co in comments:
715 paths[co.f_path][co.line_no].append(co)
714 paths[co.f_path][co.line_no].append(co)
716 return paths
715 return paths
717
716
718 @classmethod
717 @classmethod
719 def needed_extra_diff_context(cls):
718 def needed_extra_diff_context(cls):
720 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
719 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
721
720
722 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
721 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
723 if not CommentsModel.use_outdated_comments(pull_request):
722 if not CommentsModel.use_outdated_comments(pull_request):
724 return
723 return
725
724
726 comments = self._visible_inline_comments_of_pull_request(pull_request)
725 comments = self._visible_inline_comments_of_pull_request(pull_request)
727 comments_to_outdate = comments.all()
726 comments_to_outdate = comments.all()
728
727
729 for comment in comments_to_outdate:
728 for comment in comments_to_outdate:
730 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
729 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
731
730
732 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
731 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
733 diff_line = _parse_comment_line_number(comment.line_no)
732 diff_line = _parse_comment_line_number(comment.line_no)
734
733
735 try:
734 try:
736 old_context = old_diff_proc.get_context_of_line(
735 old_context = old_diff_proc.get_context_of_line(
737 path=comment.f_path, diff_line=diff_line)
736 path=comment.f_path, diff_line=diff_line)
738 new_context = new_diff_proc.get_context_of_line(
737 new_context = new_diff_proc.get_context_of_line(
739 path=comment.f_path, diff_line=diff_line)
738 path=comment.f_path, diff_line=diff_line)
740 except (diffs.LineNotInDiffException,
739 except (diffs.LineNotInDiffException,
741 diffs.FileNotInDiffException):
740 diffs.FileNotInDiffException):
742 comment.display_state = ChangesetComment.COMMENT_OUTDATED
741 comment.display_state = ChangesetComment.COMMENT_OUTDATED
743 return
742 return
744
743
745 if old_context == new_context:
744 if old_context == new_context:
746 return
745 return
747
746
748 if self._should_relocate_diff_line(diff_line):
747 if self._should_relocate_diff_line(diff_line):
749 new_diff_lines = new_diff_proc.find_context(
748 new_diff_lines = new_diff_proc.find_context(
750 path=comment.f_path, context=old_context,
749 path=comment.f_path, context=old_context,
751 offset=self.DIFF_CONTEXT_BEFORE)
750 offset=self.DIFF_CONTEXT_BEFORE)
752 if not new_diff_lines:
751 if not new_diff_lines:
753 comment.display_state = ChangesetComment.COMMENT_OUTDATED
752 comment.display_state = ChangesetComment.COMMENT_OUTDATED
754 else:
753 else:
755 new_diff_line = self._choose_closest_diff_line(
754 new_diff_line = self._choose_closest_diff_line(
756 diff_line, new_diff_lines)
755 diff_line, new_diff_lines)
757 comment.line_no = _diff_to_comment_line_number(new_diff_line)
756 comment.line_no = _diff_to_comment_line_number(new_diff_line)
758 else:
757 else:
759 comment.display_state = ChangesetComment.COMMENT_OUTDATED
758 comment.display_state = ChangesetComment.COMMENT_OUTDATED
760
759
761 def _should_relocate_diff_line(self, diff_line):
760 def _should_relocate_diff_line(self, diff_line):
762 """
761 """
763 Checks if relocation shall be tried for the given `diff_line`.
762 Checks if relocation shall be tried for the given `diff_line`.
764
763
765 If a comment points into the first lines, then we can have a situation
764 If a comment points into the first lines, then we can have a situation
766 that after an update another line has been added on top. In this case
765 that after an update another line has been added on top. In this case
767 we would find the context still and move the comment around. This
766 we would find the context still and move the comment around. This
768 would be wrong.
767 would be wrong.
769 """
768 """
770 should_relocate = (
769 should_relocate = (
771 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
770 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
772 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
771 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
773 return should_relocate
772 return should_relocate
774
773
775 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
774 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
776 candidate = new_diff_lines[0]
775 candidate = new_diff_lines[0]
777 best_delta = _diff_line_delta(diff_line, candidate)
776 best_delta = _diff_line_delta(diff_line, candidate)
778 for new_diff_line in new_diff_lines[1:]:
777 for new_diff_line in new_diff_lines[1:]:
779 delta = _diff_line_delta(diff_line, new_diff_line)
778 delta = _diff_line_delta(diff_line, new_diff_line)
780 if delta < best_delta:
779 if delta < best_delta:
781 candidate = new_diff_line
780 candidate = new_diff_line
782 best_delta = delta
781 best_delta = delta
783 return candidate
782 return candidate
784
783
785 def _visible_inline_comments_of_pull_request(self, pull_request):
784 def _visible_inline_comments_of_pull_request(self, pull_request):
786 comments = self._all_inline_comments_of_pull_request(pull_request)
785 comments = self._all_inline_comments_of_pull_request(pull_request)
787 comments = comments.filter(
786 comments = comments.filter(
788 coalesce(ChangesetComment.display_state, '') !=
787 coalesce(ChangesetComment.display_state, '') !=
789 ChangesetComment.COMMENT_OUTDATED)
788 ChangesetComment.COMMENT_OUTDATED)
790 return comments
789 return comments
791
790
792 def _all_inline_comments_of_pull_request(self, pull_request):
791 def _all_inline_comments_of_pull_request(self, pull_request):
793 comments = Session().query(ChangesetComment)\
792 comments = Session().query(ChangesetComment)\
794 .filter(ChangesetComment.line_no != None)\
793 .filter(ChangesetComment.line_no != None)\
795 .filter(ChangesetComment.f_path != None)\
794 .filter(ChangesetComment.f_path != None)\
796 .filter(ChangesetComment.pull_request == pull_request)
795 .filter(ChangesetComment.pull_request == pull_request)
797 return comments
796 return comments
798
797
799 def _all_general_comments_of_pull_request(self, pull_request):
798 def _all_general_comments_of_pull_request(self, pull_request):
800 comments = Session().query(ChangesetComment)\
799 comments = Session().query(ChangesetComment)\
801 .filter(ChangesetComment.line_no == None)\
800 .filter(ChangesetComment.line_no == None)\
802 .filter(ChangesetComment.f_path == None)\
801 .filter(ChangesetComment.f_path == None)\
803 .filter(ChangesetComment.pull_request == pull_request)
802 .filter(ChangesetComment.pull_request == pull_request)
804
803
805 return comments
804 return comments
806
805
807 @staticmethod
806 @staticmethod
808 def use_outdated_comments(pull_request):
807 def use_outdated_comments(pull_request):
809 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
808 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
810 settings = settings_model.get_general_settings()
809 settings = settings_model.get_general_settings()
811 return settings.get('rhodecode_use_outdated_comments', False)
810 return settings.get('rhodecode_use_outdated_comments', False)
812
811
813 def trigger_commit_comment_hook(self, repo, user, action, data=None):
812 def trigger_commit_comment_hook(self, repo, user, action, data=None):
814 repo = self._get_repo(repo)
813 repo = self._get_repo(repo)
815 target_scm = repo.scm_instance()
814 target_scm = repo.scm_instance()
816 if action == 'create':
815 if action == 'create':
817 trigger_hook = hooks_utils.trigger_comment_commit_hooks
816 trigger_hook = hooks_utils.trigger_comment_commit_hooks
818 elif action == 'edit':
817 elif action == 'edit':
819 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
818 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
820 else:
819 else:
821 return
820 return
822
821
823 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
822 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
824 repo, action, trigger_hook)
823 repo, action, trigger_hook)
825 trigger_hook(
824 trigger_hook(
826 username=user.username,
825 username=user.username,
827 repo_name=repo.repo_name,
826 repo_name=repo.repo_name,
828 repo_type=target_scm.alias,
827 repo_type=target_scm.alias,
829 repo=repo,
828 repo=repo,
830 data=data)
829 data=data)
831
830
832
831
833 def _parse_comment_line_number(line_no):
832 def _parse_comment_line_number(line_no):
834 """
833 """
835 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
834 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
836 """
835 """
837 old_line = None
836 old_line = None
838 new_line = None
837 new_line = None
839 if line_no.startswith('o'):
838 if line_no.startswith('o'):
840 old_line = int(line_no[1:])
839 old_line = int(line_no[1:])
841 elif line_no.startswith('n'):
840 elif line_no.startswith('n'):
842 new_line = int(line_no[1:])
841 new_line = int(line_no[1:])
843 else:
842 else:
844 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
843 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
845 return diffs.DiffLineNumber(old_line, new_line)
844 return diffs.DiffLineNumber(old_line, new_line)
846
845
847
846
848 def _diff_to_comment_line_number(diff_line):
847 def _diff_to_comment_line_number(diff_line):
849 if diff_line.new is not None:
848 if diff_line.new is not None:
850 return u'n{}'.format(diff_line.new)
849 return u'n{}'.format(diff_line.new)
851 elif diff_line.old is not None:
850 elif diff_line.old is not None:
852 return u'o{}'.format(diff_line.old)
851 return u'o{}'.format(diff_line.old)
853 return u''
852 return u''
854
853
855
854
856 def _diff_line_delta(a, b):
855 def _diff_line_delta(a, b):
857 if None not in (a.new, b.new):
856 if None not in (a.new, b.new):
858 return abs(a.new - b.new)
857 return abs(a.new - b.new)
859 elif None not in (a.old, b.old):
858 elif None not in (a.old, b.old):
860 return abs(a.old - b.old)
859 return abs(a.old - b.old)
861 else:
860 else:
862 raise ValueError(
861 raise ValueError(
863 "Cannot compute delta between {} and {}".format(a, b))
862 "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 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